入門書でのおさらい28・多次元配列の受け渡し、演習6-12,6-13

今日は170ページの多次元配列の受け渡しを勉強します

170ページ・多次元配列の受け渡し

170ページのlist6-16のように関数の仮引数でinta[4][3]のように要素数が最初から決められていますがこのような宣言の仕方もできるの?いままではint a[]のように空欄でしたが

結論から言うと、int a[4][3] のように要素数をしっかり書いて宣言するのは、多次元配列(2次元配列など)ではごく一般的な書き方です。

むしろ、いままで書いていた int a[] のような「要素数の省略」には、多次元配列になると厳格なルールが出てきます。

2次元配列で省略できるのは「左側」だけ

C言語のルールで、多次元配列を引数にするときは「一番左(1次元目)の要素数だけ」しか省略できません。

  • void func(int a[][3]) (2列目以降が確定していればOK)
  • × void func(int a[][]) (これだとコンピュータがデータの区切りを計算できずエラーになる)
  • void func(int a[4][3]) (今回の例文のように、両方書いてももちろんOK)
なぜ全部省略できないのか?

配列 a[4][3] は、メモリ上では12個(4×3)のデータが1列に並んでいます。
コンピュータが a[1][0] というデータを見に行こうとしたとき、「1行が3個ずつ並んでいる」という情報(右側の [3])がわからないと、どこからが2行目なのか計算できないからです。

今回の例文で [4][3] と書いている理由

今回の例文で [4][3] としっかり書いているのは、以下の意図があると考えられます。

  1. 「この関数は4行3列の行列専用ですよ」ということを読み手にハッキリ伝えるため。
  2. 一番左の [4] は省略可能ですが、あえて書くことで配列のサイズミスを防ぐ(可読性を高める)ため。

いままで int a[] と書いていたのは1次元配列だったから可能でしたが、2次元以上の場合は「右側の数字は必須」になると覚えておくとスムーズですよ。

演習6-12

演習5-10でやった行列の積の公式をつかったらいいね

4行3列の行列Aと、3行4列の行列Bを掛けて、結果の行列Cを作るときの計算式です。

【考え方のステップ】
結果の行列Cの「i行 j列」に入る一つの数字を出すために、以下の計算を行います。

C[i][j] = (A[i][0] * B[0][j]) + (A[i][1] * B[1][j]) + (A[i][2] * B[2][j])

【イメージ】

  1. 左側の行列Aから「横一行(3個)」取り出す。
  2. 右側の行列Bから「縦一列(3個)」取り出す。
  3. 端から順番に掛け算して、その合計を出す。

これをすべての行(i = 0~3)と列(j = 0~3)の組み合わせで行うため、プログラムでは「三重ループ」を使うことになります。

右辺で0,1,2と変化するところをk(0~2)のループで回すと思いだしました

行列の積はさらに詳しく演習5-10のページで解説しているので土そちらを見てみてください。

#include<stdio.h>
void mat_mul(const int a[4][3], const int b[3][4], int c[4][4])
{
    for(int  i = 0; i < 4; i++){
        for(int j = 0; j < 4; j++){
            for(int k = 0; k < 3; k++){
                c[i][j] += (a[i][k] * b[k][j]);
            }
        }
    }
}

int main(void)
{
int a[4][3],b[3][4],c[4][4];


puts("行列aの要素を入力してください");
for(int i = 0; i < 4; i++){
    for(int j = 0; j < 3; j++){
        printf("a[%d][%d]:", i, j);
        scanf("%d",&a[i][j]);
    }
}


puts("行列bの要素を入力してください");
for(int i = 0; i < 3; i++){
    for(int j = 0; j < 4; j++){
        printf("b[%d][%d]:", i, j);
        scanf("%d",&b[i][j]);
    }
}


mat_mul( a, b, c);

puts("-----行列a-----");
for(int i = 0; i < 4; i++){
    for(int j = 0; j < 3; j++){
        printf("%4d", a[i][j]);
    }
    putchar('\n');
}

puts("-----行列b-----");
for(int i = 0; i < 3; i++){
    for(int j = 0; j < 4; j++){
        printf("%4d", b[i][j]);
    }
    putchar('\n');
}

puts("-----行列a*行列b(行列c)-----");
for(int i = 0; i < 4; i++){
    for(int j = 0; j < 4; j++){
        printf("%4d", c[i][j]);
    }
    putchar('\n');
}

    return 0;
}

一点だけ、今のままだと「非常に惜しい」状態です。
原因は、計算を始める前の c[i][j] の状態にあります。

今のコードは c[i][j] += ... となっていますが、関数の中でこの計算が始まる時、c[i][j] には元々どんな数字が入っているでしょうか?

ここを解決する方法は2つあります:

  1. 計算を始める直前に c[i][j]0 でリセットする。
  2. += を使わずに計算する(あるいは1回目だけ = で代入する)。

今のループの中に、「0で初期化する一行」を足すとしたらどこに入れるのが良さそうですか?そこが修正できれば完成です!

どこに c[i][j] = 0; を入れればいいか、わかりますか?

ああそうだった。+=使うとき最初0にしなきゃだった。でもc[4][4]は呼び出すメイン関数で初期化してから渡したらダメなの?

メイン関数で初期化してから渡しても全く問題ありませんし、実際に動きます!

ただ、プログラミング(特にこの演習)の考え方として、以下の2点を意識しておくとより完璧です。

  1. 「自己完結」の原則:
    関数 mat_mul を使う人が、事前に初期化するのを忘れてしまうかもしれません。「この関数を呼び出せば、前のデータが何であれ、必ず正しい答えを書き込んでくれる」という作りにした方が、親切で安全な関数と言えます。
  2. 演習の意図:
    多くの入門書やテストでは、関数内で完結する書き方が期待されます。

もしメイン関数で初期化して渡すなら、今のコードで正解です!

もし「どんな状態で渡されても大丈夫な最強の関数」にしたいなら、先ほどのように中で c[i][j] = 0; をするのがベストです。

そっかじゃあより良い関数(自己完結型)にするために初期化も入れておくことにしましょう

c[4][4] = {0};
    for(int  i = 0; i < 4; i++){
        for(int j = 0; j < 4; j++){
            for(int k = 0; k < 3; k++){
                c[i][j] += (a[i][k] * b[k][j]);
            }
        }

ループの外で初期化してみたよ!

惜しいです!
c[4][4] = {0}; という書き方は、変数を宣言(作る)するときにしか使えないルールなんです。今回の関数では int c[4][4] は引数として「すでにあるもの」として渡されているので、その書き方はできません。

今のループ構造を活かすなら、k のループ(足し算)」を始める直前に、その場所を0にするのが一番スマートです。

ええ!そうだったんだ。じゃあこれはだめとしてどうしよう

c[i][j] = {0};をどこかに入れたらいいんだよね?

{0} は、先ほどお話しした通り宣言の時しか使えない書き方なので、ここはシンプルに c[i][j] = 0; と書くのが正解です。

じゃあ全部一気に初期化というよりループでc[i][j] = 0;ていうふうに1個づつ0を入れていくスタイルなの?

その通りです!「1個ずつ0にしてから、そこに計算結果を積み上げていく」というスタイルです。

C言語で「関数に渡された配列」を扱う場合、一気に丸ごと0にする便利な魔法のような書き方(={0} など)が関数の外では使えないため、実務的にもこのようにループの中で1個ずつ初期化するのが一般的です。

この書き方のメリットは:

  1. 確実性: メイン関数で初期化を忘れていても、この関数が責任を持って0にしてくれる。
  2. 効率的: 「0にする作業」と「計算する作業」をひとまとめにできる。

これで、どんな値(正、負、0)が入ってきても、どんな状態で配列が渡されても、完璧な答えを出す最強の mat_mul 関数になりましたね!

c[i][j] = 0;をどこに書き込むかで悩んでる。ループの中だとその都度0になっちゃわない?

鋭い指摘ですね!その「不安」はとても大事な感覚です。

結論から言うと、kのループの外側」に書けば、計算途中でリセットされることはありません。

イメージしやすいように、「i=0, j=0」のとき(C[0][0]を計算するとき)の動きをスロー再生してみます。

for(int i = 0; i < 4; i++){
    for(int j = 0; j < 4; j++){
        
        c[i][j] = 0;  // 【1】 ここで「今から計算する部屋」を空にする
        
        for(int k = 0; k < 3; k++){
            // 【2】 ここで「+=」を使って、3回分を積み上げていく
            c[i][j] += (a[i][k] * b[k][j]); 
        }
        // 【3】 kのループが終わったとき、c[0][0] には「合計値」が入っている
    }
}

なぜ「その都度リセット」にならないのか?

  • kのループ(一番内側)は、1つの部屋(たとえばc[0][0])の中身を完成させるためのループです。
  • c[i][j] = 0; は、そのkのループが始まる「前」にあります。
  • つまり、「1つの部屋の掃除をするのは、その部屋の合計計算を始める最初の1回だけ」ということになります。

もし、c[i][j] = 0;kのループの中に入れてしまうと、おっしゃる通り、最後の1個(k=2)の結果しか残らなくなってしまいます。

まとめると

  • jのループの直下に書く = 部屋ごとに1回リセット(正解
  • kのループの直下に書く = 足し算するたびにリセット(間違い

この「書く場所による違い」、しっくりきますか?

了解。なれないけど覚えておく。プログラムできたから見て。

#include<stdio.h>
void mat_mul(const int a[4][3], const int b[3][4], int c[4][4])
{
    for(int  i = 0; i < 4; i++){
        for(int j = 0; j < 4; j++){
            c[i][j] = 0;
            for(int k = 0; k < 3; k++){
                c[i][j] += (a[i][k] * b[k][j]);
            }
        }
    }
}

int main(void)
{
int a[4][3],b[3][4],c[4][4];


puts("行列aの要素を入力してください");
for(int i = 0; i < 4; i++){
    for(int j = 0; j < 3; j++){
        printf("a[%d][%d]:", i, j);
        scanf("%d",&a[i][j]);
    }
}


puts("行列bの要素を入力してください");
for(int i = 0; i < 3; i++){
    for(int j = 0; j < 4; j++){
        printf("b[%d][%d]:", i, j);
        scanf("%d",&b[i][j]);
    }
}


mat_mul( a, b, c);

puts("-----行列a-----");
for(int i = 0; i < 4; i++){
    for(int j = 0; j < 3; j++){
        printf("%4d", a[i][j]);
    }
    putchar('\n');
}

puts("-----行列b-----");
for(int i = 0; i < 3; i++){
    for(int j = 0; j < 4; j++){
        printf("%4d", b[i][j]);
    }
    putchar('\n');
}

puts("-----行列a*行列b(行列c)-----");
for(int i = 0; i < 4; i++){
    for(int j = 0; j < 4; j++){
        printf("%4d", c[i][j]);
    }
    putchar('\n');
}

    return 0;
}

完璧です!素晴らしいですね。

三重ループの構造、関数の呼び出し、そして行列を表示する際の「改行」や「桁数指定(%4d)」まで、非常にきれいに書けています。

ここが特に素晴らしい点です:

  • 関数内での初期化: c[i][j] = 0; を適切な位置に配置したことで、どんな値が入力されても正確に計算できる堅牢なプログラムになっています。
  • ユーザーへの配慮: printf("a[%d][%d]:", i, j); のように、今どこを入力しているか分かりやすく表示されているので、使い勝手も良いです。
  • 表示の工夫: putchar('\n'); を使って行列の形に整えているので、計算結果が正しいかどうかも一目で分かりますね。

これで「行列の積」の課題は完全クリアです!

演習6-13

あれこれこねくり回す解答

これは以前解いた演習5-12を参考にしながら解いたらいいね。

tensu[2][4][3]の3次元配列を考える場合[2]はテスト回数、[4]は学生の数、[3]は教科の数ですね。

    int tensu[2][4][3] = {
        {
            {91, 63, 78}, 
            {67, 72, 46}, 
            {89, 34, 53}, 
            {32, 54, 34}
        },
        {
            {97, 67, 82},
            {73, 43 ,46}, 
            {97, 56, 21}, 
            {85, 46, 35}
        }
    };

ステップ1:3次元配列の宣言

今は tensu1[4][3]tensu2[4][3] がありますが、これを1つにまとめると、以下のようになります。
「2回分 × 4人 × 3教科」なので、要素数は [2][4][3] です。

このように、main関数の中にある tensu1tensu2 を、1つの tensu という名前に書き換えるところから始めてみてください。

次に悩むのは、「この3次元配列を関数にどう渡すか(関数の書き換え)」になるかと思います。

ステップ2:表示関数の書き換え

今のコードには mat_print という「2次元配列を表示する関数」がありますが、これを「3次元配列の中の『指定した1回分』を表示する関数」に作り変える必要があります。

ここが少し難しいところです。関数の引数(受け取り方)をどう書き換えたら良いか、イメージは湧きますか?

元のコードにある void mat_print(const int m[4][3]) という書き方に注目してください。
これは「4行3列の2次元配列を受け取りますよ」という意味です。

今回の3次元配列 tensu[2][4][3] の場合、tensu[0] と書くと「0番目の【4行3列の2次元配列】」を指すことになります。

ですので、main関数の中での呼び出し方はこうなります:

puts("1回目の点数"); mat_print(tensu[0]); // 0番目の4行3列を渡す
puts("2回目の点数"); mat_print(tensu[1]); // 1番目の4行3列を渡す

これなら、mat_print 関数自体は、元のコードから1文字も書き換えずにそのまま使えます!

void mat_print(const int m[4][3])定義では2次元配列を受け取るようにしてるのに3次元配列を書いてもいいの?

鋭い指摘ですね!おっしゃる通り、3次元配列そのもの(tensuをそのまま渡そうとすると、型が違うのでエラーになります。

でも、「添字」を1つだけ指定すると、魔法のようなことが起こります。

ここがポイント

3次元配列 int tensu[2][4][3] において:

  • tensu と書くと、それは「2×4×3」の3次元配列全体を指します。
  • tensu[0] と書くと、それは「1回目」の4行3列の2次元配列を指します。
  • tensu[1] と書くと、それは「2回目」の4行3列の2次元配列を指します。

つまり、mat_print 関数が int m[4][3](2次元配列)を待っているところに、tensu[0] を放り込んであげれば、型がぴったり一致して正しく動くんです。

puts("1回目の点数");
mat_print(tensu[0]); // 3次元の「0番目の面(2次元)」を渡す

puts("2回目の点数");
mat_print(tensu[1]); // 3次元の「1番目の面(2次元)」を渡す

OK、そのやり方は一つの極スマートなやり方としていいと思う!!でもさ書き換えるのが課題なのに元のコードから1文字も書き換えずにそのまま使えます!っていうのはいいの?ほかに方法ないの?

確かにそうですね!「3次元配列を扱うプログラム」に書き換えるのが目的なので、関数側も「3次元配列をドサッと丸ごと受け取る形」に作り変えたほうが、「3次元配列を使いこなしている感」が出ます。

今の mat_print は「4×3の平面」を1枚受け取るルールですが、これを「2×4×3の立体」をまるごと受け取るルールに書き換える方法があります。

関数の定義(受け取り側)の書き換え

関数の入り口をこう書き換えます。

void mat_print(const int m[2][4][3])
{
    // ここでループを「3重」にする必要があります
    for (int i = 0; i < 2; i++) {
        printf("%d回目の点数\n", i + 1);
        for (int j = 0; j < 4; j++) {
            for (int k = 0; k < 3; k++) {
                printf("%4d", m[i][j][k]);
            }
            putchar('\n');
        }
    }
}

こうすると、main関数からは mat_print(tensu); と書くだけで、全データを一気に表示できるようになります。

どちらの方向に進みますか?

方法は2つあります。

  • プランA(関数も3次元対応に改造):
    関数側で「2回分ループ」も全部やってしまう。mainはスッキリする。
  • プランB(関数は2次元のまま、mainで制御):
    関数は「1面分を表示する」という部品のままにしておき、main側で tensu[0]tensu[1] と指定して呼び出す。

「3次元配列に格納するように書き換えたプログラムを作成せよ」という問題の意図を汲むなら、プランA(関数自体を3次元配列を受け取る形に改造する)のほうが「書き換えた!」という達成感があるかもしれません。

どちらのやり方で進めてみたいですか?

うーんどちらに進んでもダメと思います。こうするとこの関数はsumの表示に使えなくなるからです。

なるほど!鋭いですね。その通りです。

もし mat_print を「3次元配列を丸ごと受け取って2回分一気に表示する」形(プランA)にしてしまうと、2次元配列である sum(合計点)を表示するときに困ってしまいますね。

sum は「4人×3教科」の 1枚の面(2次元) しかないので、3次元用の関数に放り込むと型が合わなくなります。

ということは、このプログラムを成立させるには、以下のどちらかの道を選ぶ必要がありそうです。

  1. データの持ち方を変える:
    sum も3次元配列 sum[1][4][3] のように宣言する(ちょっと不自然ですが…)。
  2. 関数の役割を分ける:
    mat_print は「2次元の面を表示する」という今の役割(部品)のままにしておき、main 側で tensu[0]tensu[1] を渡して使い回す。

sum に使えない」という問題を解決するには、どちらの道がスッキリすると思いますか?あるいは、もっと別の「第3の道」があるでしょうか。

私なら2番を採用して、mat_print2(2次元用表示) mat_print3(3次元用表示)を作ってわけます。わかりにくいからです。わかりやすくはなるけど回りくどい回答になりますね。

「3次元配列を扱う関数(mat_print3)」と「2次元配列を扱う関数(mat_print2)」を分けることで、役割がハッキリしてコードの読みやすさがグッと上がります。

では、その方針で進めてみましょう。

ステップ3:表示用の関数の作成

  1. mat_print3 (3次元用)
    tensu[2][4][3] を丸ごと受け取って、ループを「3重」にして2回分のテスト結果を表示する。
  2. mat_print2 (2次元用)
    これは元の mat_print とほぼ同じです。sum[4][3] を受け取って表示する。

プリント部をこうしました

void mat_print2(const int m[4][3])
{
    puts("合計点");
        for(int j = 0; j < 4; j++){
            for(int k = 0; k < 3; k++){
                printf("%4d",m[j][k]);
            }
            putchar('\n');
        }
}

void mat_print3(const int m[2][4][3])
{
    for(int i = 0; i < 2; i++){
        printf("%d回目の点数\n",i + 1);
        for(int j = 0; j < 4; j++){
            for(int k = 0; k < 3; k++){
                printf("%4d",m[i][j][k]);
            }
            putchar('\n');
        }
        putchar('\n'); 
    }
}

いいですね!それぞれの役割が明確で、演習として非常に誠実なコードです。
特に mat_print33重ループ (i, j, k) の使い方は、3次元配列の構造を完璧に捉えています。

mat_print2 の中に puts("合計点"); を入れたのも、呼び出し側がスッキリするので良い判断だと思います。

呼び出し部はこうです

    mat_print3(tensu);
    mat_print2(sum);

呼び出し部分、非常に分かりやすいです!
3次元配列をドサッと渡す mat_print3 と、計算結果の2次元配列を渡す mat_print2。これならメイン関数(main)の流れがとても綺麗に見えますね。

ステップ4:足し算用の関数の作成

では、仕上げの mat_add です。
今のままだと tensu1tensu2 という変数はもう存在しないので、呼び出し方を工夫する必要があります。

今の mat_add の定義(中身)を変えずに、main 関数の中で呼び出すとしたら、どのようになりますか?

// main関数内での呼び出し
mat_add(tensu[0], tensu[1], sum);

このように、「3次元配列の何番目を使うか」を指定して渡す形にするか、あるいは mat_add 自体も 3次元配列を受け取る形 に作り変えてしまうか。

どちらで完成させたいですか?

君の提示したやつは最初の
puts(“1回目の点数”); mat_print(tensu[0]);
の考えのやつとセットで使おうね。そのプログラムも作るから

おっしゃる通りです!私の提案した a[0][j][k] のような書き方は、「関数側で0番目と1番目を明示的に指定して足す」 という設計ですね。

一方で、あなたが最初に気づいた mat_print(tensu[0]) のような呼び出し方は、「関数側は2次元のことだけを考え、呼び出し側がどの面を渡すか決める」 という、非常に汎用性の高い(使い回しがきく)設計になります。

両方のパターンを作ることで、「データ構造を関数に押し込めるか、呼び出し側で制御するか」 という設計の違いが体感できて、最高の勉強になりますね。

私はまず全部三次元配列の受け渡しにしたいからその案に付き合ってね。下のコードでどうかな??

void mat_add(const int a[2][4][3], int b[4][3])

その形、今回のプログラムの構造にピッタリです!

引数をそのように定義すると、役割が非常に明確になりますね。

  • a: 2回分のデータが入った「3次元配列」をまるごと受け取る。
  • b: 計算結果を書き込むための「2次元配列(sum)」を受け取る。

この定義でいく場合、関数の中の足し算の式はどうなりますか?
a0番目」と「a1番目」を足して、結果を b に入れることになります

b[j][k] = a[0][j][k] + a[1][j][k];

あなたが以下のようにしたいのは十分にわかりました

    for(int j = 0; j < 4; j++){
        for(int k = 0; k < 3; k++){
            b[j][k] = a[0][j][k] + a[1][j][k];
        }
    }

まさにその通りです!その書き方こそが、「3次元配列を丸ごと受け取って、その内部構造(0番目と1番目)を直接操作する」という設計の正解です。

そのコードのポイントは:

  • 引数 a が3次元なので、アクセスするときも a[0][j][k] のように添字が3つ必要であること。
  • 代入先の b は2次元なので、b[j][k]添字が2つであること。

この違いを正確に書き分けられているのが素晴らしいです。

これでパーツがすべて揃いましたね!

  1. 3次元配列の宣言tensu[2][4][3]
  2. 3次元用表示関数mat_print3
  3. 2次元用表示関数mat_print2
  4. 3次元から合計を出す関数mat_add

これらを一つのプログラムにまとめれば完成です。

実際にこれらを組み合わせて動かしてみた際、実行結果はイメージ通りになりそうですか?(1回目の各要素と2回目の各要素が正しく足されて表示されるはずです)

しかし私はテストが2回で終わるとは思ってません。何なら回数を0、1で指定したとこのその続きは2,3,4でループできそうです

その通りです!「0, 1」と直接数字を書かずに、そこを変数 i に置き換えてループさせれば、何回テストが増えても対応できますね。

こんなイメージでしょうか。

for (int i = 0; i < 2; i++) { // この「2」を回数分にする
    for (int j = 0; j < 4; j++) {
        for (int k = 0; k < 3; k++) {
            // ここで足し算を繰り返す
        }
    }
}

b[j][k] += a[i][j][k];この一文を使うことになるからb[j][k]を0にしなければなりません。この3重ループの場合の回答を教えてください

あえて「答え」をそのまま書きますね。もし i(回数)のループを一番外側に持ってきた場合、「初期化」と「足し算の書き方」がこう変わります。

void mat_add(const int a[2][4][3], int b[4][3])
{
    // 1. まず、合計用の配列を全部ゼロでクリアしておく必要がある
    for (int j = 0; j < 4; j++) {
        for (int k = 0; k < 3; k++) {
            b[j][k] = 0;
        }
    }

    // 2. そのあとで、回数分(i)を足していく
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 4; j++) {
            for (int k = 0; k < 3; k++) {
                b[j][k] += a[i][j][k];
            }
        }
    }
}

まず初期化の処理をしてから足していくスタイルですね。

ループの中で b[j][k] = 0; と書く場所がなくなり、最初に一括で掃除(初期化)しておく必要が出てきます。

なるほど初期化問題をどうするかが一番悩みどころになりますね

まさにその通りです!「どこでリセットするか」という問題ですね。

これはどうでしょう

    for(int j = 0; j < 4; j++){
        for(int k = 0; k < 3; k++){
            b[j][k] = 0;
            for(int i = 0; i < 2; i++){
                b[j][k] += a[i][j][k];
            }
        }
    }

なぜ「回数のループ(i)を一番内側」 に持ってきたのか、その設計の意図は非常に明確で合理的です。

一言で言うと、「一人の生徒の、一つの教科」に注目して、その合計を出し切ってから次に進むという思考プロセスに基づいているからです。

具体的には以下のようなメリットがあります。

  1. 初期化のタイミングが完璧:
    b[j][k] = 0;(合計をリセットする作業)を、特定の「生徒jの教科k」を計算する直前で行えます。これにより、他の生徒や他の教科の点数が混ざる心配がありません。
  2. メモリへの書き込みが効率的:
    一度 b[j][k] という場所(例:1人目の数学の合計欄)を決めたら、そこに2回分(i=0, 1)のデータを全て足し終えてから、次の欄へ移動します。この「一箇所を完成させてから次へ」という流れは、非常に直感的です。
  3. 「回数」がいくら増えても対応できる:
    この構造なら、テストが10回になっても「生徒jの教科k」について10回分足し算を繰り返すだけなので、ロジックが破綻しません。

もしこれが、回数のループ(i)が一番外側にあったら、b[j][k] = 0; をどこに置くかでもっと苦労したはずです。

処理の順番が1回目と2回目の同じ場所を足してから次の場所に移るっていうの考えだね。

その通りです!「1回目と2回目の同じ場所(同じ生徒の同じ教科)をセットにして処理し、一箇所ずつ確実に片付けていく」という思考回路ですね。

この考え方の利点は、頭の中の意識が「点」に集中できることです。

  1. [0][0](1人目の英語)に注目:1回目と2回目を足して完了!
  2. [0][1](1人目の数学)に注目:1回目と2回目を足して完了!
    …という風に、「一歩ずつ地面を固めて進む」ような着実さがあります。

完成形はこちらです

#include<stdio.h>
void mat_add(const int a[2][4][3], int b[4][3])
{
    for(int j = 0; j < 4; j++){
        for(int k = 0; k < 3; k++){
            b[j][k] = 0;
            for(int i = 0; i < 2; i++){
                b[j][k] += a[i][j][k];
            }
        }
    }
}

void mat_print2(const int m[4][3])
{
    puts("合計点");
        for(int j = 0; j < 4; j++){
            for(int k = 0; k < 3; k++){
                printf("%4d",m[j][k]);
            }
            putchar('\n');
        }
}

void mat_print3(const int m[2][4][3])
{
    for(int i = 0; i < 2; i++){
        printf("%d回目の点数\n",i + 1);
        for(int j = 0; j < 4; j++){
            for(int k = 0; k < 3; k++){
                printf("%4d",m[i][j][k]);
            }
            putchar('\n');
        }
        putchar('\n'); 
    }
}

int main(void)
{
    int tensu[2][4][3] = {
        {
            {91, 63, 78}, 
            {67, 72, 46}, 
            {89, 34, 53}, 
            {32, 54, 34}
        },
        {
            {97, 67, 82},
            {73, 43 ,46}, 
            {97, 56, 21}, 
            {85, 46, 35}
        }
    }; 
    int sum[4][3];

    mat_add(tensu, sum);
    mat_print3(tensu);
    mat_print2(sum);

    return 0;
}

もっともスマートな現場で好まれる回答

こねくり回したけど一番スマートなプログラムも書いてみました!君が最初に「つまり、mat_print 関数が int m[4][3](2次元配列)を待っているところに、tensu[0] を放り込んであげれば、型がぴったり一致して正しく動くんです。」とお勧めしてくれたやつです

#include<stdio.h>

void mat_add(const int a[2][4][3], int b[4][3])
{
    for(int j = 0; j < 4; j++){
        for(int k = 0; k < 3; k++){
            b[j][k] = a[0][j][k] + a[1][j][k];
        }
    }
}

void mat_print(const int m[4][3])
{
        for(int i = 0; i < 4; i++){
            for(int j = 0; j < 3; j++){
                printf("%4d",m[i][j]);
            }
            putchar('\n');
        }
}

int main(void)
{
    int tensu[2][4][3] = {
        {
            {91, 63, 78}, 
            {67, 72, 46}, 
            {89, 34, 53}, 
            {32, 54, 34}
        },
        {
            {97, 67, 82},
            {73, 43 ,46}, 
            {97, 56, 21}, 
            {85, 46, 35}
        }
    }; 
    int sum[4][3]; 

    mat_add(tensu, sum);
    puts("1回目の点数"); mat_print(tensu[0]);
    puts("2回目の点数"); mat_print(tensu[1]);
    puts("合計点");   mat_print(sum);

    return 0;
}

これこそが「部品(関数)の汎用性を最大限に活かした」、プロの現場でも好まれるスマートな書き方ですね。

このプログラムの素晴らしいところは、mat_print という関数が「4行3列のデータなら何でもござれ」というスタンスでいることです。

  • 3次元配列の1枚目 (tensu[0]) を渡してもOK
  • 3次元配列の2枚目 (tensu[1]) を渡してもOK
  • 計算結果の2次元配列 (sum) を渡してもOK

このように、「呼び出し側(main)でどのデータを渡すか制御する」ことで、関数を一つにまとめ、コード全体の行数をスッキリ削ぎ落としています。

「こねくり回して」両方のパターンを自分で書いたことで、

  1. 3次元構造を真正面から扱う重厚な設計
  2. 2次元の部品を賢く使い回す軽快な設計
    の両方の武器を手に入れましたね。これは大きな一歩です!

今日の演習はこれでコンプリートでしょうか?

はい、結構大変でしたこの課題。ではまたねありがとう