多次元配列

2次元以上の配列を作ることもできる。単に、サイズの指定を増やせばいいだけだ。

int ary2[4][5];
int ary3[3][3][3];

これらの配列は、メモリ上にはより右に書いてあるの次元の配列が連続して割り当てられる。言い換えると、最も低いアドレスから高いアドレスに順にアクセスしていくと、より右の添字のほうが早く変化する。
4次元以上についても同様。もっとも、そんな次元の配列を使う機会はそうなないだろう。

次のプログラムは、4×5の2次元配列を作って各添字の積を格納してから、行列形式で表示する。

#include

int main(void)
{
    int twod[4][5];
    int i, j;

    for (i = 0; i < 4; i++) {
        for (j = 0; j < 5; j++) {
            twod[i][j] = i * j;
        }
    }
    for (i = 0; i < 4; i++) {
        for (j = 0; j < 5; j++) {
            printf("%d ", twod[i][j]);
        }
        printf("\n");
    }
}
takatoh@nightschool $ ./sample_5_3
0 0 0 0 0 
0 1 2 3 4 
0 2 4 6 8 
0 3 6 9 12

文字列

C では文字列は、文字の配列だ。最もよく使われる1次元配列は文字列だそうだ。文字列は1文字ずつ配列の要素に格納され、最後にヌル文字(0)がつく。
文字列に関する関数をいくつか見てみよう。

gets()

gets() はキーボードから文字列を読み取る。ユーザーが文字列に続いて Enter キーを押すと、gats() は改行文字をヌル文字に置き換えて返す。

#include

int main(void)
{
    char str[80];
    int i;

    printf("Input string (less than 80).: ");
    gets(str);
    for (i = 0; str[i]; i++) {
        printf("%c", str[i]);
    }
    printf("\n");

    return 0;
}
takatoh@nightschool $ gcc sample_5_2a.c -o sample_5_2a
sample_5_2a.c: In function ‘main’:
sample_5_2a.c:10:5: warning: ‘gets’ is deprecated (declared at /usr/include/stdio.h:638) [-Wdeprecated-declarations]
     gets(str);
     ^
/tmp/ccMGfyBh.o: 関数 `main' 内:
sample_5_2a.c:(.text+0x2e): 警告: the `gets' function is dangerous and should not be used.

おっと、何やら警告が出た。gets は危険だから使うべきじゃない、と。まあ、そのとおりらしい。本には gets は文字の長さをチェックしないので受け入れる配列におさまるかどうかプログラマが注意しなければいけない、と書いてある。
コンパイル自体はできてるようだし、今回はサンプルなのでこのまま使うことにしよう。

takatoh@nightschool $ ./sample_5_2a
Input string (less than 80).: Hello, this is Takatoh!
Hello, this is Takatoh!

このプログラムでは、文字列終端のヌル文字が 0 (偽)であることを利用している。

strcpy()

文字列ソースをターゲットにコピーする。string.h が必要。

strcpy(ターゲット, ソース);

使い方はこんな感じ。

char str[80];

strcpy(str, "Hello, world.");
printf("%s\n", str);

strcat()

文字列ターゲットにソースを連結する。string.h が必要。

strcar(ターゲット, ソース);

次のようにすると、「hello, threr」と表示する。

char str[80];

strcat(str, "hello,");
strcat(str, " there");

printf(str);

strcmp()

2つの文字列を比較する。string.h が必要。

strcmp(文字列1, 文字列2);

文字列はコード順に大きさを比較(コードが小さい方の文字列が小さいと判定される)され、文字列1と文字列2が同じ大きさなら 0 を返す。文字列1のほうが小さければ 0 より小さい値を、大きければ 0 より大きい値を返す。

strlen()

文字列の長さを返す。ヌル文字は含まれない。string.h が必要。

strlen(文字列);

練習問題

次のプログラムは、文字列を読み取り、それを逆の順序で表示する。

#include
#include

int main(void)
{
    char str[80];
    int i;

    printf("Input string (less than 80): ");
    gets(str);

    i = strlen(str);

    for ( ; i; i--) {
        printf("%c", str[i - 1]);
    }
    printf("\n");

    return 0;
}
takatoh@nightschool $ ./practice_5_2
Input string (less than 80): hello, world.
.dlrow ,olleh

配列

(1次元の)配列を宣言する一般的な形式は次の通り。

型 変数名[サイズ];
  • 配列の要素はすべて同じ型
  • 要素にアクセスするには変数名に大カッコ([])で囲んだ添字をつけてアクセスする
  • 添字は 0 から始まる
  • 配列は連続したメモリ領域に格納される
  • 配列をまるごと代入(コピー)することは出来ない。要素を1つずつ代入(コピー)する

要素を1つずつコピーしなきゃいけないのな面倒だな。

次のプログラムは、1 から 10 までの平方値を、配列 sqrs に格納してから表示する。

#include

int main(void)
{
    int sqrs[10];
    int i;

    for (i = 1; i < 11; i++) {
        sqrs[i - 1] = i * i;
    }
    for (i = 0; i < 10; i++) {
        printf("%d ", sqrs[i]);
    }
    printf("\n");

    return 0;
}
takatoh@nightschool $ ./sample_5_1
1 4 9 16 25 36 49 64 81 100

もうひとつ、大事なこと。
C では配列の添字の範囲をチェックしない。例えばサイズ 10(添字は 0〜9)の配列の11番目の要素(添字10)にもアクセスできてしまう。もちろん正しい動作にはならないので、場合によってはプログラムがクラッシュするかもしれない。
ちょっと試してみよう。

#include

int main(void)
{
    int ary[10];
    int i;

    for (i = 1; i < 11; i++) {
        ary[i - 1] = i;
    }

    printf("%d\n", ary[10]); /* out of range */

    return 0;
}
takatoh@nightschool $ gcc sample_5_1a.c -o sample_5_1a
takatoh@nightschool $ ./sample_5_1a
0

何事もないかのようにコンパイルされたけど、実行すると、代入したはずのない ary[10] の出力として、0 と表示された。
何度か試してみたけど、毎回 0 と表示される。これはたまたまかもしれないし、gcc がそうしているのかもしれない。どちらにせよ、配列の要素にアクセスするときには添字の範囲に注意が必要だ。

型キャスト

値の型を明示的に変換する方法を型キャスト(type cast)という。型キャストの一般亭な形式は次の通り。

(型) 値

次の例では、浮動小数点数を整数として表示するために int に型キャストしている。

#include

int main(void)
{
    float f;

    f = 100.5;

    printf("%d\n", (int) f);

    return 0;
}
takatoh@nightschool $ ./sample_4_7
100

代入文での型変換

代入分の左辺と右辺の型が異なる場合、右辺の型が左辺の型に変換される。もし、左辺が右辺よりも大きな型であれば問題はない。その逆の場合は問題ありで、例えば次のプログラムは、「1000」と表示せずに「-24」と表示する。

#include

int main(void)
{
    char ch;
    int i;

    i = 1000;
    ch = i;

    printf("%d\n", ch);

    return 0;
}
takatoh@nightschool $ ./sample_4_6a
-24

なぜなら、int の i を char の ch に代入する際、i の下位 8 ビットだけがコピーされるから。C ではこのような代入での型変換はエラーにならない。代入文で、大きな整数から小さな整数(char を含む)に変換されるとき、上位ビットが失われるのが基本的な規則。

また、大きな浮動小数点数から小さな浮動小数点数への変換では精度が落ちるし、浮動小数点数から整数への変換では小数部が失われる。
値が変換後の型におさまらない場合は、結果は意味のない値になってしまう。

次の2点は重要。

  • int から float、float から double への変換は、値の表現を変えるだけで精度を上げるものでなないこと。
  • コンパイラによって、char を符号付きとして扱うものと符号なしで扱うものがある。このため 127 よりも大きな値を代入した時の結果は処理系に依存する。これが問題になる場合は明示的に signed あるいは unsigned と宣言すべき。

次のプログラムでは、浮動小数点数を整数に代入することで小数部が失われることがわかる。

#include

int main(void)
{
    int i;
    float f;

    f = 1234.0098;
    i = f;
    printf("%f %d\n", f, i);

    return 0;
}
takatoh@nightschool $ ./sample_4_6b
1234.009766 1234

む、浮動小数点数の精度がおかしいな。とはいえ、整数に代入することで、小数部が失われることは分かった。

式内部での型変換

C では1つの式の中に異なる型を混在させることができる。これは、式中に異なる方が現れた時に、どのように解消するかが決められているから。

整数拡張

式中に char や short int が使われていると、その式が評価されるときに必ず int に拡張される。ただし、この拡張は式の評価時だけの一時的なものであって、変数自体が拡張されるわけではないので注意。

型の昇格

整数拡張のあと、コンパイラはすべてのオペランドを一番大きなオペランドに合わせて変換する。これを型の昇格という。昇格の規則は次の通り。上から順に見ていって、当てはまる変換が行われる。

一方のオペランドの型もう一方のオペランドが昇格する型
long doublelong double
doubledouble
floatfloat
unsigned longunsigned long
longlong
unsignedunsigned

要するに、大きな方のオペランドの型にもう一方も変換されるってことだ。

1つ、特殊なケースがある。一方が long、もう一方が unsigned int で、しかもその unsigned int の値を long で表せない場合、両方が unsigned long に変換される。

次のプログラムは、int と float の演算をしているけど、変数 i が float に変換されて、出力は float になる。

#include

int main(void)
{
    int i;
    float f;

    i = 10;
    f = 23.25;

    printf("%f\n", i * f);

    return 0;
}
takatoh@nightschool $ ./sample_4_5a
232.500000

もう1つ。型変換の規則は演算のひとつひとつについて適用されるので、次のプログラムでは、(10 / 3) は整数であり、100.0 を割るときに初めて float に変換されるので、答えは 33.3333… になる(3.0 にはならない)。

#include

int main(void)
{
    printf("%f\n", 100.0 / (10 / 3));

    return 0l;
}
takatoh@nightschool $ ./sample_4_5b
33.333333

変数の初期化

変数を宣言するときに、ついでに初期化することができる。一般的な形式は次の通り。

型 変数名 = 値;

グローバル変数の初期化に使えるのは定数だけ、ローカル変数の初期化には定数、変数、関数呼び出し(戻り値)が使える。
グローバル変数は最初に1回だけ初期化され、ローカル変数は関数が呼び出されるたびに初期化される。
明示的に初期化されないグローバル変数は 0 に設定されるけど、ローカル変数の場合は値が不定になる。

変数をまとめて宣言するとき、初期化する変数としない変数を混ぜることができる。

int min = 0, count, max = 100;

次のプログラムでは、main 関数の中のローカル変数 y をグローバル変数 x で、ローカル変数 z を関数の戻り値で初期化している。

#include

int x = 10; /* global */

int myfunc(int i);

int main(void)
{
    int y = x; /* initialize with global variable */

    int z = myfunc(y); /* initialize with function */

    printf("%d %d\n", y, z);

    return 0;
}

int myfunc(int i)
{
    return i / 2;
}
takatoh@nightschool $ ./sample_4_4
10 5

定数

ここでいう定数は、名前のついた定数、例えば Ruby の FOO とかのことじゃなくて、いわゆるリテラルのこと。

  • 整数の定数は小数点と小数部がない数値。例:10、-100
  • 浮動小数点数の定数は、小数点と小数部がついた数値。例:10.0、-2.5
  • 浮動小数点数の定数は、指数表記もできる。例:1.25E+03
  • 文字定数は、引用符(’ ‘)で囲む。例:’a’
  • 文字列定数は、二重引用符(” “)で囲む。例:”hogehoge”

ここでちょっと注意点。C には文字列というデータ型はない。文字列は文字の配列として扱われている。これはまた後で出てくるはず。

コンパイラが数値の定数を解釈するとき、デフォルトでは互換性のあるデータ型のうちその数値がおさまる最小の型だと解釈する。ただし、浮動小数点数は double と解釈する。これを変えたいときは次のような接尾辞をつける。

  • 浮動小数点数:F をつけると float、L をつけると long double
  • 整数:U をつけると unsigned、L をつけると long int

数値を変数に代入するときには、その変数の型に解釈してくれるのであまり気にすることはないけど、関数の引数として直接使うときには注意が必要。次のプログラムは、「42340」ではなく「-23196」と出力してしまう。

#include

int main(void)
{
    printf("%hd\n", 42340);

    return 0;
}
takatoh@nightschool $ ./sample_4_3
-23196

これは、%hd という指定子から引数の数値が short int だと解釈してしまうから。正しく出力するには、%hu 指定子(符号なし short int)を使う必要がある。

変数の宣言場所

変数を宣言する場所と変数の有効範囲は次の通り。

  • 関数の外で宣言するとグローバル変数
  • 関数の中で宣言するとその関数だけのローカル変数
  • コードブロックの中で宣言するとそのブロックだけのローカル変数

また、次の制約がある。

  • 変数は使うより前に宣言しなければいけない

関数の外だとグローバル、中だとローカルってのはいいとして、コードブロックのローカル変数ってのは使いどころがよくわからないな。でも本には「実際には、ほとんどのCプログラマは1つの関数で使用するすべての変数をその関数のブロックの最初に宣言します。」って書いてあるから、そうしておけば良さそう。

グローバル変数とローカル変数は同じ名前でも構わない。有効な範囲にローカル変数があればそれが使われる。なければグローバル変数が使われる。
次のプログラムには、同じ count という名前のグローバル変数とローカル変数が使われているけど、それぞれ独立している。

#include

void f1(void);

int count; /* global */

int main(void)
{
    count = 10;
    f1();
    printf("value of count in main(): %d\n", count);

    return 0;
}

void f1(void)
{
    int count; /* local */

    count = 1000;
    printf("value of count in f1(): %d\n", count);
}
takatoh@nightschool $ ./sample_4_2
value of count in f1(): 1000
value of count in main(): 10

データ型修飾子とデータのサイズ

void 以外の基本データ型には修飾子をつけることによって、よりきめ細かく指定することができる。データ型修飾子には次の4つがある。

  • long
  • short
  • signed
  • unsigned

long と short はそれぞれデータサイズが変わる。といっても ANSI の規定が厳密じゃないみたいなので、処理系によって違うらしい。signed と unsigned は符号つきか符号なしかの違い。省略すると符号つきと解釈される。
データ型修飾子はそれぞれ適用できるデータ型が決まっているので、表にまとめてみた。

shortint
longint, double
signedchar, int
unsignedchar, int

それぞれのデータサイズは次のプログラムで確認してみる(ググって調べた)。

#include

int main(void)
{
    printf("char = %lu\n", sizeof(char));
    printf("int = %lu\n", sizeof(int));
    printf("short int = %lu\n", sizeof(short int));
    printf("long int = %lu\n", sizeof(long int));
    printf("float = %lu\n", sizeof(float));
    printf("double = %lu\n", sizeof(double));
    printf("long double = %lu\n", sizeof(long double));

    return 0;
}
takatoh@nightschool $ gcc --version
gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

takatoh@nightschool $ ./sample_4_1
char = 1
int = 4
short int = 2
long int = 8
float = 4
double = 8
long double = 16

単位はバイト。