【おさらいC言語】第2回:メイン関数のargcとargvの形をイメージしよう!

全13回でお届けする【おさらいC言語】シリーズ。第2回目のテーマは、C言語のメイン関数がコマンドラインから受け取ることができるパラメータargcargvの構造についてです。これらの中に何のデータがどのように格納されるのかを、イメージしやすいように解説していきます。また、ほかのプログラミング言語との違いについても紹介します。

コマンドライン引数(コマンドライン・パラメータ)とは

コマンドライン引数とは、ターミナル(コマンドライン)からコマンドやプログラムを起動する時に渡すことができる引数(パラメータ)のことをいいます。ターミナルから実行できるプログラムにはさまざまなものがありますが、その多くではコマンドライン引数としてオプションやファイル名などを与えられるようになっています。

前回(第1回)の記事で、ハローワールドのプログラムをターミナルからコンパイルする例を紹介しました。次のようにgccを実行すると、a.outという名前の実行ファイルが作られます。この例では、main.cの部分が引数です。

$ gcc main.c

引数が2つ以上あるときは、次のようにスペースで区切って指定します。

$ gcc -o main main.c

この例では-omainmain.cの3つが引数です。gccでは-oは出力(output)先を指定するオプションで、-o mainという2つの引数の並びで「出力する実行ファイル名を(a.outではなく)mainにする」という指定になります。

ワンポイント
ターミナルから実行できるさまざまなコマンドには、コマンドライン引数を与えられるようになっている。

C言語のコマンドライン引数の構造

コマンドライン引数は、自分でコンパイルしたプログラムにも与えることが可能です。ここからは、C言語で書いたプログラムを実行したときに、コマンドライン引数がどのような構造で受け取られるのかをみていきましょう。

C言語でコマンドライン引数を受け取るには

例えば、ハローワールドの実行ファイルa.outを起動するときにabcdefgの2つの引数を指定したい場合は、ターミナルで次のように入力して実行します。

$ ./a.out abc defg
Hello, world.

しかし、コマンドライン引数を与えても与えなくても、ハローワールドの動作は変わりません。なぜなら、ハローワールドの中で使われているメイン関数は、コマンドライン引数を無視しているからです。メイン関数には2通りのプロトタイプがありますが(詳しくは第1回の記事を参照)、コマンドライン引数を受け取るプログラムを書くには、次のようにパラメータ付きのメイン関数を使う必要があります。

int main(int argc, char *argv[]) {
	// argcとargvの2つのパラメータでコマンドライン引数を受け取っている
}

これで、プログラムが受け取ったコマンドライン引数は、メイン関数のパラメータとしてargcargvの2つの変数に格納されるようになります。読みづらい変数名ですが、前者は「引数の数(argument count)」、後者は「引数の配列(argument vector)」の略です。配列とは、同じ形式のデータが一列に並んだようなデータ構造のことです。データを配列として格納しておくと、先頭から何番目にあるのか(インデックス)さえわかれば、どのデータにでも素早くアクセスすることができます。そのため、プログラミングでは配列が頻繁に使われています。

ワンポイント
C言語でコマンドライン引数を受け取るには、パラメータ付きのメイン関数を使う。

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だということがわかります。

ワンポイント
C言語の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]);
    }
}

メイン関数のargvchar *argv[]と少し複雑な形になっていますが、これは文字列の配列だということを表す書き方のひとつです。配列のインデックスは0からはじまるので、最初の文字列にはargv[0]とすればアクセスできます。また、配列の個数がargcに格納されることから、最後の文字列はargv[argc-1]となります。これを利用すれば、forループでargvに含まれるすべての文字列にアクセスすることが可能です。

上のプログラムでは、forループによって変数iの値を0からargc-1まで変化させながらprintfを実行しています。printfのフォーマットに%sという指定が出てきましたが、これは文字列(String)を当てはめるためのものです。iは数値なので%dargv[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つ目以降の値をチェックすればよいということになります。

ワンポイント
C言語の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言語がわかれば、ほかのプログラミング言語への理解も深まる。

文字列の配列を正確にイメージしよう

ここからは、話をC言語に戻します。メイン関数のargvは文字列の配列でした。では、C言語における文字列の配列とは、どのような構造をしたデータなのかを整理していきます。

文字列の配列とは

先程、配列とは「同じ形式のデータが一列に並んだような構造」をしていると説明しました。ですから、文字列の配列とは、文字列が一列に並んでいる構造だと想像できます。では、これを図に描いてみるとどのようになるでしょうか。次のような構造をイメージする人が多いかもしれません。

上記のようなイメージは概念図としては間違っていませんが、C言語のデータ構造としては正解とはいえません。C言語における文字列の配列とは、より正確には「文字列を指すポインタの配列」だからです。これを踏まえて文字列の配列を表現すると、次の図のようになります。

少し難しくなってきましたので、順を追って説明していきます。特に「ポインタ」はC言語の学習者がつまずきやすいといわれている箇所ですが、正しいイメージさえできればそれほど難しいものではありません。

ワンポイント
文字列の配列とは、文字列を指すポインタの配列のこと。

文字列の構造

しっかりとイメージを固めるために、まずは文字列について理解しましょう。文字列とは、文字がずらっと並んで列になったデータのことです。そのため、「文字の配列」として表すことができます。C言語の言葉でより正確に表現するなら、「ヌル文字で終わるchar型の配列」です。char型というのは文字(character)を格納するための整数のことで、文字のひとつひとつに対応づけられた番号によって、半角の英数字や記号を1文字だけ表現できます。また、char型は「ヌル(null)」という特別な値をとることもできます。

char型の値が配列となって並び、その最後の要素がヌルになっているのが文字列です。図にすると、下記のようなイメージです。

ヌル文字は、C言語ではバックスラッシュを使って\0と書くことになっています。ただし、日本語環境ではバックスラッシュの部分が半角の円記号(¥)になる場合があります。「null」は「ナル」と書いたほうが実際の発音に近いのですが、慣例的に「ヌル」と書くことが多いようです。

ちなみに、nullには「何もない」という意味があります。第1回の記事で出てきたvoidも同じような意味を持つ単語です。nullが「何もないことを示すデータ」であるのに対し、voidは「データすらない」と捉えると違いがわかるでしょう。

ワンポイント
C言語の文字列とは、終端がヌルになった文字の配列のこと。

文字列の配列とポインタ

文字列の構造がわかったので、argvのような「文字列の配列」についてあらためて考えてみましょう。文字列が「文字の配列」だということから、「文字列の配列」=「文字の配列の配列」と考えるとどうなるでしょうか。図にすると、下記のようなイメージです。

上記は「2次元配列」と呼ばれる構造です。この場合、すべての文字列に同じサイズの領域を用意する必要があります。しかし、文字列の長さはバラバラなので、どうしても無駄な領域ができてしまいます。このような無駄をなくすためには、ポインタが便利です。

ポインタには、データの「アドレス」を格納することができます。プログラムから直接アクセスできるデータは、intcharも、それらの配列であってもすべてメモリ上に配置されます。また、ただ配置するだけでは見失ってしまうので、どこにあるのかがわかるように必ずアドレスが割り振られます。そのため、ポインタにアドレスを格納しておけば、ポインタがデータを指し示すようになり、どのようなデータにでもたどり着くことができます。

文字列のアドレスも、ポインタに格納することができます。そのため、「文字列の配列」を「文字列を指すポインタの配列」として表現することが可能です。図にすると、下記のようなイメージです。

ポインタを使うことで、無駄な領域がなくなったことがわかるでしょうか。これが、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言語のメイン関数がコマンドラインから受け取ることができるパラメータargcargvには、コマンドライン引数のサイズとその一覧が入っています。ここまで読んで、これらの構造がなんとなくイメージできたのではないでしょうか。しかし、ポインタのイメージはまだ少し曖昧な状態かもしれません。より正確なイメージをもつには、データやポインタがメモリの中でどうなっているのかを想像できるようになることが大切です。これはC言語だけでなく、いろいろなプログラミング言語への理解を深めるためにも重要な部分だといえます。

次回は、メイン関数のパラメータargvがメモリの中でどのようになっているのかについて、詳しく説明していきます。
準備中