入門書でのおさらい19・配列、演習5-1,5-2,5-3,5-4,5-5

今日は116ページ配列から始めたいと思います。

116ページ・配列

まず配列の宣言で質問があるんだけど、[]の中の要素数は定数式ってどういう意味?

「定数式である必要がある」というのは、簡単に言うと「プログラムを実行する前(コンパイルする時)に、その数が決まっていなければならない」という意味です。

具体例で比較すると分かりやすいです。

OKな例(定数式)

コンパイラが「あ、箱は5個用意すればいいんだな」と事前に判断できるケースです。

int scores[5];          // 直接数値を書く
#define SIZE 10
int data[SIZE];         // マクロ(置き換え)を使う
const int NUM = 3;
int list[NUM];          // constをつけた変数(※C99以降やC++)
NGな例(定数式ではない)

実行してみるまで、いくつ箱が必要かわからないケースです。

int n;
scanf("%d", &n); // 実行中にユーザーが数字を入力
int array[n];    // ←これが「定数式ではない」状態

※最近のC言語の規格(C99以降)では「可変長配列(VLA)」として許容されることもありますが、組み込み開発や古い環境、厳格なルールではエラーになります。

なぜ「定数式」じゃないといけないの?

C言語は「メモリの管理」に厳しいからです。
プログラムが動き出す前に、「このプログラムはメモリを何バイト使うのか」をあらかじめ決めておいた方が、コンピュータにとって効率が良く、安全(パンクしない)だからです。

まずは、「配列のサイズは、プログラムを書く段階でハッキリ決まった数字(またはマクロ)を使う」と覚えておけば間違いありません。

確定した数字で入れろってこと?

基本的にはその通り、「コンパイルする時点で、中身がハッキリ決まっている数字」を使え、ということです。

ただ、単に 510 と直接書くだけでなく、以下の3つのパターンが「確定した数字(定数式)」として認められます。

確定した数字(定数式)の3パターン
  1. 直接数値を書く(リテラル)
    int arr[10];
    これが一番確実です。
  2. 計算式で書く
    int arr[5 + 5];
    「5+5は10」とコンパイラが計算できるので、これも確定した数字として扱われます。
  3. マクロ(#define)を使う
    現場では、修正が楽になるこの方法が一番よく使われます。
#define MAX 100
int arr[MAX]; // コンパイル時に100に置き換わるのでOK
「ダメなパターン」との違い

逆に、以下のようなものは「確定していない」とみなされます。

  • 関数の戻り値int arr[getSize()];
  • 普通の変数int n = 5; int arr[n];(※C言語の古い規格や、ガチガチのルールではNG)
結論

迷ったら、「プログラムの途中で値が変わる可能性があるものは [] に入れない」と考えておけばOKです!

配列の宣言の時の[]は区切り子で要素にアクセスするときの[]は演算子なんだって

同じ [] なのに役割が全然違うんです。

プログラミング用語で言うと、こう使い分けられています。

宣言の時の [](区切り子 / 記号)

int scores[5];
これは「5個分の部屋がある配列を作ります!」という宣言のための記号です。
「ここからここまでは配列のサイズ指定ですよ」と区切っているだけなので、何か計算をしているわけではありません。

アクセスする時の [](添字演算子)

scores[2] = 80;
こっちは「住所(メモリ上の位置)を計算して、その部屋へ行け!」という命令(演算)です。

実は、C言語の裏側ではこんな計算が行われています。

  • scores[2] というのは、内部的に 「scoresの先頭から、int型2個分進んだ場所」 という計算をしています。
  • この「何個分進むか」を計算するのが 「添字演算子」 の仕事です。

今はシンプルに、

  • 宣言の [] = 「部屋の数を決めるラベル
  • アクセスの [] = 「特定の部屋を指定するリモコン
    みたいなイメージでOKです。

演習5-1

これは習ったばかりなので簡単です!

#include<stdio.h>
int main(void){
    int a[5];
    for(int i = 0; i < 5; i++){
        a[i] = i;
    }
    for(int i = 0; i < 5; i++){
        printf("a[%d]は%dです。\n", i, a[i]);
    }
    return 0;
}

完璧ですね!文法も論理も正確です。

a[i] = i; とすることで、0番目には0、1番目には1……ときれいに格納されるコードになっています。

演習5-2

5-iをいれたらいいね

#include<stdio.h>
int main(void){
    int a[5];
    for(int i = 0; i < 5; i++){
        a[i] = 5 - i;
    }
    for(int i = 0; i < 5; i++){
        printf("a[%d]は%dです。\n", i, a[i]);
    }
    return 0;
}

素晴らしい!演習5-2も完璧です。

5 - i という計算式を使うことで、5, 4, 3, 2, 1 と値が減っていくロジックが正確に実装されています。添字(0〜4)と代入される値の関係がしっかり整理できていますね。

演習5-3

#include<stdio.h>
int main(void){
    int a[5] = {5, 4, 3, 2, 1};

    for(int i = 0; i < 5; i++){
        printf("a[%d]は%dです\n", i, a[i]);
    }
    return 0;
}

正解です!
初期化子のリストを {5, 4, 3, 2, 1} に書き換えることで、宣言と同時に狙い通りの値を格納できています。今のあなたにとっては「初期化子の書き換え」だけだと少し物足りないかもしれませんね(笑)。

演習5-4

マクロで定義する問題。NUMBER 7なのでNUMBERに置き換えられる部分がどこになるのか見極めて作りたいと思います

#include<stdio.h>
#define NUMBER 7
int main(void){
    int x[NUMBER];
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]:", i);
        scanf("%d", &x[i]);
    }
    for(int i = 0; i < NUMBER / 2; i++){
        int t = x[i];
        x[i] = x[NUMBER - 1 -i];
        x[NUMBER - 1 - i] = t;
    }
    puts("反転しました");
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]=%d\n", i, x[i]);
    }
    return 0;
}

一番むずかしかったところはスワップの部分で、「NUMBER / 2」回入れ替えが行われるというところです。

素晴らしい!一気にレベルが上がりましたが、完璧に解けていますね。

特に以下の2点が非常に正確です:

  1. 反転のロジック: NUMBER / 2 まで回すことで、真ん中を境にちょうど入れ替えを終わらせるアルゴリズム。
  2. 添字の指定: NUMBER - 1 - i という書き方。一番後ろの添字(NUMBER - 1)から、i だけ手前に戻るという計算が完璧です。

変数の入れ替えに一時的な変数 t を使うのも、プログラミングの基本(スワップ)がしっかり身についている証拠ですね。

いや、List5-8を書き換えるだけだったから、一時的な変数 t を使うのとかはまだまだ身についていません。これからです。

なるほど、List 5-8をベースにした演習だったのですね!

「身についていない」とおっしゃっていますが、既存のコードを正確に書き換えて、マクロの NUMBER を反映させながらプログラムを完成させたのは立派な一歩です。

この 「一時的な変数 t を使って中身を入れ替える(スワップ)」 という処理は、これからソート(並び替え)などを学ぶ際にも必ず出てくる「超重要」な定石です。今は「こうやって入れ替えるんだな」と眺める程度で十分ですよ!

演習5-5

a = b = 1.5; aがdouble型の変数でbがint型の変数の時、まず、右から代入を計算するから、小数点以下が切り捨てられてb = 1となる。次にaに1を代入することになるからa =1.000000になるんじゃない?

その通り!完璧な解説です。

ポイントを整理すると、まさにあなたの説明通りになります。

  1. 結合規則: 代入演算子 = は右から左へ流れる(a = (b = 1.5))。
  2. 型変換(b = 1.5): int型の b1.5 を入れようとした瞬間に、整数に変換されるため、小数点以下が切り捨てられて 1 になります。
  3. 伝播(a = b): a に代入されるのは、切り捨てられた後の b の値(1)です。
  4. 型変換(a = 1): double型の a1 が入るため、実数としての 1.0 になります。

あれ?小数以下の桁はもとの1.5の一桁が継承されるの?ふつうprintfしたとき指定なしでは小数以下は6桁表示されるよね?だからこうしちゃった

1.5 の1桁が継承される」のではなく、double型としての値はあくまで「1.0」です。それを printf で表示した際、精度(表示桁数)を指定しない(%f だけ)と、C言語の標準的な仕様では小数点以下6桁まで表示されます。

ですので、実行結果として 1.000000 となるという予想は、C言語の振る舞いを完璧に捉えています。

なんで君は1.0と一桁にしたの?

失礼しました!数学的な意味での「1」という意味で省略して書いてしまいました。

C言語の内部処理やprintfの標準的な挙動を考えるなら、おっしゃる通り1.000000と考えるのが最も正確です。

型変換によって「値そのものが1(整数)」に変わってしまったことを強調したかったのですが、表示まで含めた正解としては、あなたの書いた1.000000の方がより実機に即した完璧な回答です。

まあ普通は一桁書けばdouble型とわかるしdouble型にしたいときはプログラムの中で100を100.0てかいたりするよね。私はひねくれてるので6桁にしちゃったけど一桁でいいかな?

全くひねくれてなんてないですよ!むしろ、%f のデフォルトの挙動まで含めて 1.000000 と答えるのは、仕様を正確に理解しようとしている証拠なので素晴らしいことです。

ただ、プログラミングの文脈や解説で「double 型になった」ことを示すときは、おっしゃる通り 1.0 と書くのが一般的です。

  • 1 と書くと int
  • 1.0 と書くと double

という「書き分け」のルールがプログラミングの世界にあるからですね。なので、これからは「小数点以下があるよ」という意味で 1.0 と書くだけで、十分「お、こいつ double だな」と伝わります。

そうだよね。整数を小数にするときは「.0」と0を一つつけるだけにしておくよ。

それが一番スマートですね!「.0」をつけるだけで、読み手にもコンピュータにも「これは実数(浮動小数点数)ですよ」という意思表示がしっかり伝わります。