Visual StudioによるC/C++プログラミング:基礎からデータ構造・数値計算まで(授業資料)
全体構成
| 区分 | 回 | 主要トピック |
|---|---|---|
| 基礎編 | ce-1〜ce-4 | プログラミング入門,開発環境,変数・式・入出力,制御構造 |
| 中間まとめ1 | ce-5 | 基礎編の総復習 |
| データ構造編 | ce-6〜ce-10 | ファイル・配列,メモリ配置,構造体,ポインタ・連結リスト,二分探索木 |
| 中間まとめ2 | ce-11 | データ構造編の総復習 |
| 数値計算編 | ce-12〜ce-14 | ニュートン法・数値積分,計算精度と誤差,行列・線形方程式 |
学習の到達目標
本シリーズを修了することで,以下の能力を習得できる。
第一に,C言語の基本構文と制御構造を理解し,プログラムを作成できる。第二に,ポインタ,連結リスト,二分探索木などのデータ構造を実装できる。第三に,数値計算の基礎(方程式の数値解法,数値積分,LU分解による連立一次方程式の解法)を理解し,プログラムとして実装できる。第四に,計算精度と誤差の概念を理解し,数値計算における注意点を把握できる。
ce-1. C プログラミング入門
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
例題1:棒グラフ
目的
関数の定義方法と呼び出し方を理解する。整数の長さに応じてアスタリスクを繰り返し表示する処理を、独立した関数として実装する。
手順
- 新しいCソースファイルを作成する
#include "stdio.h"と#include <math.h>をファイル先頭に記述する- 関数barを定義する
- 戻り値の型は
void(値を返さない関数であることを示す) - 引数として
int lenを受け取る - 関数内でループ変数
int iを宣言する - for文を使い、i=0からi<lenの間、
printf("*")を繰り返す - ループ終了後、
printf("\n")で改行を出力する return;で関数を終了する
- 戻り値の型は
- main関数を定義する
- 変数
int len、char buf[256]、int chを宣言する printf("len =")でプロンプトを表示するfgets(buf, 256, stdin)でキーボード入力を読み込むsscanf_s(buf, "%d\n", &len)で入力文字列から整数を取得するbar(len)で作成した関数を呼び出すreturn 0;でプログラムを終了する
- 変数
- コンパイルして実行し、「6」を入力して
******が表示されることを確認する
ヒント
- 関数barはmain関数より前に記述する(C言語では、呼び出す前に関数が定義されている必要がある)
- for文の条件式
i<lenは「iがlenより小さい間」を意味する。i=0から始めてlen回繰り返す - セミコロンの付け忘れに注意する。各文の末尾には必ずセミコロンが必要である
例題2:平方根の計算
目的
if-else文による条件分岐を理解する。入力値が負の場合と非負の場合で処理を分岐させる方法を学ぶ。
手順
- 新しいCソースファイルを作成する
#include "stdio.h"と#include <math.h>をファイル先頭に記述する- main関数を定義する
- 変数
double x、double y、char buf[256]、int chを宣言する printf("x=")でプロンプトを表示するfgets(buf, 256, stdin)でキーボード入力を読み込むsscanf_s(buf, "%lf\n", &x)で入力文字列から浮動小数点数を取得する
- 変数
- if文で条件分岐を実装する
- 条件式
x < 0が成り立つ場合:「負なので計算できません」と表示する - 条件式が成り立たない場合(else):
y = sqrt(x)で平方根を計算し、結果を表示する
- 条件式
return 0;でプログラムを終了する- コンパイルして実行し、以下の動作を確認する
- 「9」を入力 →
sqrt(9.000000)=3.000000と表示される - 「-1」を入力 → 「負なので計算できません」と表示される
- 「9」を入力 →
ヒント
- double型(浮動小数点数型)の入力には書式指定子
%lfを使用する - sqrt関数は
<math.h>ヘッダに含まれる数学関数で、引数の平方根を返す - if文の条件式を囲む括弧と、処理ブロックを囲む波括弧を区別する
例題3:繰り返し計算とファイル出力
目的
for文による繰り返し処理と、ファイルへのデータ書き出し方法を理解する。
手順
- 以下の機能を確認する
- 計算の繰り返し
- キーボードからのデータ読み込み
- ファイルへの書き出し
- これまでに学んだfor文の構造を復習する
- 初期式:ループ変数の初期化
- 条件式:繰り返しを継続する条件
- 再設定式:各反復後のループ変数の更新
- 繰り返し処理を含むプログラムを作成する
- ファイル出力機能を追加する
ヒント
- ループ変数(ループカウンタ)は繰り返し回数を管理するための変数である
- インクリメント演算子
i++は変数iの値を1増やす操作を表す - デクリメント演算子
i--は変数iの値を1減らす操作を表す
ce-2. Microsoft Visual C++ を使ってみよう
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
課題1:Microsoft Visual Studio C++でのプログラム作成と実行
目的
Microsoft Visual Studio C++を使用して、C++ソースファイルの編集からビルド、テスト実行、さらにExcelとの連携までの一連の開発プロセスを体験する。
手順
本課題は、例題1(プログラム実行の体験)と例題2(他のソフトとの連携)の2つのパートで構成される。
パート1:プログラムの作成と実行(例題1)
Step 1:Microsoft Visual Studio C++の起動
- 「スタート」メニューを開く
- 「プログラム」から「Visual Studio 2015」を選択する
- 初回起動時は初期設定画面が表示されるので、指示に従って設定を完了する
Step 2:プロジェクトの新規作成
- 「ファイル」メニューから「新規作成」→「プロジェクト」を選択する
- プロジェクト新規作成のウインドウが表示される
- 左側のリストから「Visual C++」を選択し、「Win32コンソールアプリケーション」を選ぶ
- 「プロジェクト名」欄に任意の名前を入力する
- 「場所」欄に保存先フォルダを指定する(USBメモリやネットワークドライブを使用する場合は適切に設定する)
- 「OK」をクリックする
- Win32アプリケーションウィザードが表示されたら「完了」をクリックする
- プロジェクトが作成されたことを確認する
Step 3:C++ソースファイルの編集
- 画面中央の編集領域に、プログラムを入力する
- プログラム中の「d:¥¥data.csv」は、自分の環境に合わせて適切なファイルパスに変更する
Step 4:ビルドと実行
- 「ビルド」メニューから「ソリューションのビルド」を選択する
- 画面下部にビルド結果が表示される。「1 正常終了,0 失敗」と表示されれば成功である
- エラーが発生した場合は、エラーメッセージを確認してプログラムを修正し、再度ビルドを行う
- ビルド成功後、「デバッグ」メニューから「デバッグ無しで開始」を選択する
- 実行画面が表示されたら、「start_x =」の後に「0」を入力してEnterキーを押す
- 「step_x =」の後に「0.1」を入力してEnterキーを押す
- 計算結果(sin関数の値)が20回分表示されることを確認する
- 指定したパスにdata.csvファイルが作成されていることを確認する
パート2:Excelとの連携(例題2)
Step 5:Microsoft Excelでグラフ作成
- Step 4で作成されたdata.csvファイルをダブルクリックして開く
- Microsoft Excelが起動し、計算結果のデータが表示される
- グラフにしたいデータ範囲をマウスでドラッグして選択する
- グラフウィザードのアイコンをクリックする
- グラフの種類として「散布図」を選択する
- グラフの形とデータ範囲を確認し、「次へ」をクリックする
- 必要に応じて凡例などを設定し、「次へ」をクリックする
- グラフの作成場所を選択し、「完了」をクリックする
- sin関数の波形グラフが表示されることを確認する
Step 6:終了処理
- 作成したExcelファイルを保存する
- Microsoft Visual Studio C++で「ファイル」→「終了」を選択して終了する
ヒント
よくある間違いと回避方法
- 全角文字の混入:プログラム中に全角のセミコロン「;」などが混入すると、「文字'0x81'は認識できません」というエラーが発生する。エディタの表示だけでは判別しにくいため、エラーが発生した場合は該当行を注意深く確認する。
- ファイルパスの設定:「d:¥¥data.csv」のパスは環境によって異なる。書き込み権限のあるフォルダを指定する必要がある。
- ビルド前の実行:ビルドが正常終了していない状態で「デバッグ無しで開始」を選択しても意味がない。必ずビルドの成功を確認してから実行する。
- 別のプログラムを作成する場合:次の課題に取り組む際は、ソースファイルを書き換えるだけではエラーになる。1つのプロジェクトにはメイン関数が1つしか許されないため、新しいプロジェクトを作成する必要がある。
考察のポイント
- プログラムの各部分(キーボードからのデータ読み込み、計算処理、ファイルへの書き出し)がどのように対応しているか理解する。
- for文による繰り返し処理で、変数iが0から19まで変化しながら20回の計算が行われる仕組みを確認する。
- CSVファイル(Comma-Separated Values:カンマ区切りのテキストファイル)を介してプログラムとExcelが連携する仕組みを理解する。
補足:プログラムの理解を深めるために
補足説明資料として別の例題プログラムが掲載されている。このプログラムは、キーボードから2つの整数を読み込み、四則演算と型のキャスト(データ型の変換)を行うものである。課題1を完了した後、理解を深めるために取り組むことを推奨する。
特に、整数同士の除算結果を実数に変換する場合と、実数に変換してから除算する場合の違いは、プログラミングにおける重要な概念である。
また、ブレークポイント(プログラムの実行を一時停止させる地点)の設定方法も習得しておくとよい。デバッグ時に活用できる機能である。
ce-3. 変数,式,入力,出力
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
コードの可読性について
プログラムを書く際は,ブロック単位での字下げ(インデント)を行うことが重要である。forループやif文などの制御構造の内部は,一段階字下げすることで,プログラムの構造が視覚的に把握しやすくなる。本講義のサンプルコードでも,波括弧 { } の内部が字下げされていることを確認してほしい。
例題1:自由落下距離
目的
自由落下の物理公式をCプログラムで実装し,計算結果をCSVファイルに出力する方法を学ぶ。forループによる繰り返し処理とファイル入出力の基本を習得する。
手順
- Microsoft Visual Studioを起動し,Win32コンソールアプリケーションを新規作成する
- C++ソースファイルを開き,プログラムコードを入力する
- データファイル名
d:\\data.csvを自分の環境に合わせて適切なパスに変更する - 「ビルド」→「ソリューションのビルド」を実行し,エラーがないことを確認する
- 「デバッグ」→「デバッグなしで開始」を実行する
- 表示されるウィンドウで start_x に「0」,step_x に「0.1」を入力する
- 指定したドライブに data.csv ファイルが作成されたことを確認する
- Excelで data.csv を開き,計算結果を確認する
ヒント
- 自由落下距離の公式は y = (1/2) × g × t² であり,プログラム中では
y = (9.8 / 2.0) * x * xと表現されている - ビルド時に「1 正常終了」と表示されない場合は,コードの入力ミス(特にセミコロンや括弧の対応)を確認する
- ファイルパス中の
\\はエスケープシーケンス(特殊な意味を持つ文字の組み合わせ)であり,1つのバックスラッシュを表す
例題2:三角形の面積
目的
キーボードから数値を読み込み,計算結果を画面に表示する基本的な入出力プログラムを作成する。浮動小数点型(double型)の変数宣言と代入文の使い方を習得する。
手順
- 新規プロジェクトを作成し,プログラムコードを入力する
- ビルドを実行し,エラーがないことを確認する
- プログラムを実行する
- teihen に「5」,takasa に「3」を入力する
- 表示された menseki の値が「7.5」であることを確認する
- 異なる値(例:底辺 2.5,高さ 5)を入力し,結果が「6.25」になることを確認する
ヒント
- scanf関数(キーボードから値を読み込む関数)では書式に
%lfを使用し,変数名の前に&を付ける - printf関数(画面に値を表示する関数)では書式に
%fを使用し,変数名の前に&を付けない - 代入文の
=は「等しい」という意味ではなく,「右辺の計算結果を左辺の変数に格納する」という意味である \nは改行を意味するエスケープシーケンスである
例題3:sin関数による三角形の面積
目的
標準ライブラリ関数(あらかじめ用意されている関数群)を利用した計算プログラムを作成する。三角関数における角度単位の変換(度からラジアンへ)の必要性を理解する。
手順
- 新規プロジェクトを作成し,プログラムコードを入力する
- プログラム先頭に
#include <math.h>が記述されていることを確認する - ビルドを実行し,エラーがないことを確認する
- プログラムを実行する
- a,b,theta にそれぞれ値を入力する(例:a = 10,b = 10,theta = 30)
- 計算結果 S が正しいことを手計算で検証する
ヒント
- 三角関数
sin()はラジアン(弧度法)を単位とするため,度からラジアンへの変換が必要である - 変換式は「ラジアン = 度 × π / 180」であり,プログラム中では
theta * 3.14159 / 180.0と表現されている 180.0のように小数点を付けることで,浮動小数点数として計算されることをコンパイラに伝えている- 数学関数を使用するには,必ず
#include <math.h>をプログラム先頭に記述する
標準ライブラリ関数一覧(参考)
| 分類 | 関数 | 機能 |
|---|---|---|
| 指数・対数・平方根 | exp |
指数関数(eを底とする指数zの累乗,eのz乗) |
log |
対数関数(底をeとする自然対数の計算) | |
sqrt |
平方根 | |
| 三角関数 | sin |
正弦 |
cos |
余弦 | |
tan |
正接 | |
asin |
逆正弦 | |
acos |
逆余弦 | |
atan |
逆正接 | |
| その他 | fabs |
絶対値 |
fmod(x,y) |
浮動小数データの剰余 | |
pow(x,y) |
べき乗(xのy乗) |
ce-4. プログラムの実行順序
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
例題1:自由落下距離のステップ実行
目的
Microsoft Visual Studio C++のステップ実行機能を使用して、プログラムの実行順序を理解し、変数の値がどのように変化するかを観察する。
手順
- 前回の授業で作成した「自由落下距離」のプロジェクトをMicrosoft Visual Studio C++で開く。
- ソースコードを確認し、プログラムと一致していることを確認する。データファイル名(
d:\\data.csv)は自分の環境に合わせて適切に設定する。 - 「ビルド」メニューから「ソリューションのビルド」を選択し、ビルドを実行する。
- 画面下部に「1 正常終了」と表示されることを確認する。正常終了しない場合は構文エラー(syntax error:プログラムの文法的な誤り)を確認する。
- Microsoft Visual Studio C++のウインドウ内にマウスカーソルを置いた状態で「F10」キーを押し、ステップ実行を開始する。
- 実行ウインドウが開き、マークが移動することを確認する。
- 「F10」キーを繰り返し押し、マークが1行ずつ進むことを観察する。
- 変数の値(x, y, i, start_x, step_xなど)が画面に表示されるので、値の変化を観察する。
- 「start_x=」のメッセージが表示されたら、実行ウインドウで「0」と入力してEnterキーを押す。
- 同様に「step_x=」に対して「0.1」と入力してEnterキーを押す。
- for文(繰り返し処理を行う制御構文)の中で、iが0から19まで変化しながら、xとyの値が計算されることを観察する。
- プログラムの終了まで「F10」キーでステップ実行を続け、全体の流れを把握する。
ヒント
ステップ実行中に変数の値を観察する際は、特にfor文の繰り返し処理に注目するとよい。変数iが増加するたびにxとyがどのように計算されるかを追うことで、プログラムの動作原理を理解できる。また、プログラムは「メイン関数から実行開始」し、「順次実行」されることを意識する。
例題2:平方根の計算
目的
if文とelse文による条件分岐の仕組みを理解し、入力値に応じて異なる処理を実行するプログラムを作成する。
手順
- Microsoft Visual Studio C++で新しいプロジェクトを作成する。
- ソースコードを入力する。
- ビルドを実行し、正常終了することを確認する。
- 通常実行(「デバッグ」→「デバッグなしで開始」)でプログラムを実行する。
- 「x=」のプロンプトに対して正の数(例:9)を入力し、Enterキーを押す。
- 平方根の計算結果(例:sqrt(9.000000)=3.000000)が表示されることを確認する。
- 再度プログラムを実行し、負の数(例:-1)を入力する。
- 「負なので計算できません」というメッセージが表示されることを確認する。
- ステップ実行(F10キー)を使用して、正の数を入力した場合と負の数を入力した場合で、プログラムの実行経路が異なることを確認する。
ヒント
条件分岐では「if ( x < 0 )」という条件式が評価される。条件式が真(true)の場合はif文のブロック内が実行され、偽(false)の場合はelse文のブロック内が実行される。実行順の番号を参照しながらステップ実行すると、条件分岐の動作を視覚的に理解できる。比較演算子(<, <=, >, >=, ==, !=)の意味を確認すること。
例題3:棒グラフでのステップ実行
目的
複数の関数を含むプログラムにおいて、関数呼び出しと戻り(return)の流れを理解する。
手順
- Microsoft Visual Studio C++で新しいプロジェクトを作成する。
- ソースコードを入力する。このプログラムはbar関数とmain関数の2つの関数で構成されている。
- ビルドを実行し、正常終了することを確認する。
- F10キーを押してステップ実行を開始する。
- main関数の先頭から実行が始まることを確認する。
- 「len=」のプロンプトに対して整数(例:5)を入力し、Enterキーを押す。
- F10キーでステップ実行を続け、「
bar( len );」の行に到達する。 - この時点でF10キーを押すと、bar関数の内部には入らず、bar関数の実行が完了した後の行に進むことを確認する。
- 実行ウインドウに「*****」(入力した数だけのアスタリスク)が表示されていることを確認する。
- プログラムの終了まで観察を続ける。
ヒント
F10キーによるステップ実行では、関数呼び出しがあっても関数内部には入らず、関数全体を1ステップとして扱う。関数内部の動作を詳しく観察したい場合は、例題3(b)でステップイン機能を使用する。実行順の番号を参照すると、main関数とbar関数の間の制御の流れを理解しやすい。
voidやint)、関数名、引数リストの記述に誤りがないか確認すること。また、関数呼び出し時の引数の型と、関数定義の仮引数の型が一致していることを確認する。
例題3(b):棒グラフでのステップイン
目的
ステップ実行(F10キー)とステップイン(F11キー)の違いを理解し、関数内部の処理を詳細に追跡できるようになる。
手順
- 例題3で作成したプロジェクトを開く。
- F10キーを押してステップ実行を開始する。
- 「len=」に整数(例:5)を入力してEnterキーを押す。
- F10キーでステップ実行を続け、「
bar( len );」の行に到達する。 - この行でF10キーではなく「F11」キーを押す。
- マークがbar関数の内部(
void bar( int len )の次の行)に移動することを確認する。 - F10キーでbar関数内部のステップ実行を続ける。
- for文の繰り返しの中で、
printf("*")が複数回実行されることを観察する。 - return文に到達すると、main関数の「
bar( len );」の次の行に戻ることを確認する。 - プログラムの終了まで観察を続ける。
ヒント
F10キー(ステップ実行)は関数呼び出しを1ステップとして扱い、F11キー(ステップイン)は関数内部に入って詳細な動作を追跡する。プログラムの流れの図を参照すると、関数呼び出しとreturnによる制御の移動を視覚的に理解できる。ステップインは、関数の内部動作をデバッグしたい場合に有効である。
return;」と記述し、int型の関数では「return 0;」のように戻り値を指定する。セミコロンの忘れにも注意する。
ce-5. 中間まとめ1
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
例題1:論理的エラーの発見と解決
目的
構文エラー(syntax error)がなくビルドは成功するが、実行結果が期待通りにならない「論理的エラー(logical error)」を発見し、修正する方法を学ぶ。
手順
- プログラムをVisual Studioで新規プロジェクトとして入力する
- 「ビルド」→「ソリューションのビルド」を実行し、「1. 正常終了」のメッセージを確認する
- 「デバッグ」→「デバッグなしで開始」を実行する
- 実行ウィンドウで「start_x = 0」「step_x = 0.1」と入力する
- dドライブに生成された「data.csv」をダブルクリックして開く
- 出力結果が期待値(sinカーブの値)と異なることを確認する
- プログラム中のprintf文およびfprintf文の書式指定子を確認する
- 変数x, yの型(double)に対応する正しい書式指定子に修正する
- 再度ビルド・実行し、正しい結果が得られることを確認する
ヒント
- 書式指定子(format specifier)とは、printf系関数で変数の値を出力する際に型を指定する記号である。double型の出力には「%f」を使用する
- 「%d」は整数(int型)用の書式指定子であり、double型の変数に使用すると不正な値が表示される
- ビルドが成功しても、書式指定子の型不一致は警告されない場合があるため注意が必要である
例題2:繰り返しと条件分岐
目的
while文による無限ループとif文による条件分岐、break文によるループ脱出を組み合わせた制御構造を理解する。
手順
- プログラムをVisual Studioで入力する
- ビルドを実行し、正常終了を確認する
- プログラムを実行し、正の整数を複数回入力して足し算の動作を確認する
- 負の数を入力し、プログラムが終了することを確認する
- プログラムの各部分の役割を以下の観点で確認する
- 「while (1)」の意味
- 「if (n >= 0)」の条件が成立する場合の処理
- 「else」節内の「break;」の動作
ヒント
- 「while (1)」は条件式が常に真となるため、無限ループを形成する。ループを終了させるにはbreak文が必要である
- break文は、それを囲む最も内側のループ(whileまたはfor)から抜け出す
- 変数sumは累積値を保持するため、ループ開始前に0で初期化する必要がある
課題1:データファイル出力への書き換え
目的
例題2のプログラムを改造し、計算結果をファイルに出力する処理を実装する。
手順
- 例題2のプログラムをコピーして新規ファイルとして保存する
- ファイル操作に必要な変数(FILE型ポインタ)を宣言する
- ループ開始前にfopen関数でファイル「d:\sum.csv」を書き込みモードで開く
- ループ内の計算結果表示部分に、fprintf関数によるファイル出力処理を追加する
- ループ終了後にfclose関数でファイルを閉じる
- プログラムを実行し、d:\sum.csvに結果が出力されることを確認する
ヒント
- ファイル操作の基本パターンを参照する
- fopenの第2引数「"w"」は書き込みモード(write mode)を意味する
- ファイルパスの「\\」はC言語の文字列内でバックスラッシュを表すエスケープシーケンスである
課題2:数列の和の計算
目的
forまたはwhile文を使用し、数学的な数列の和(Σk(k+1), k=1からn)を計算するプログラムを作成する。
手順
- 新規プロジェクトを作成する
- キーボードからnの値を入力する処理を記述する
- 累積変数sumを0で初期化する
- forまたはwhileを使用し、k=1からnまで繰り返す処理を記述する
- 各繰り返しでk*(k+1)を計算し、sumに加算する
- ループ終了後、sumの値を表示する
- 手計算で検証可能な小さいnの値(例:n=3)で動作確認する
ヒント
- Σk(k+1)(k=1からn)は、k=1のときk(k+1)=2、k=2のとき6、k=3のとき12となる。n=3の場合、合計は2+6+12=20である
- ループカウンタの初期値と終了条件に注意する。k=1から開始し、k≦nの間繰り返す
- 例題2の累積処理(sum = sum + n)のパターンを応用する
課題3:平均値表示への書き換え
目的
例題2のプログラムを改造し、入力値の平均値を計算・表示する機能を追加する。
手順
- 例題2のプログラムをコピーして新規ファイルとして保存する
- 入力された値の個数をカウントする変数を追加する
- 合計値を個数で除算し、平均値を計算する
- 平均値を表示する処理を追加する
- 複数の入力値で動作確認する
ヒント
- 平均値 = 合計 ÷ 個数 である
- 整数同士の除算では小数部が切り捨てられるため、平均値をdouble型で計算する場合はキャスト(型変換)が必要となる場合がある
- 個数が0の場合(入力なしで終了した場合)の除算エラーに注意する
照合確認の結果
| チェック項目 | 判定 | 根拠 |
|---|---|---|
| 演習問題の数が一致しているか | OK | 例題2問、課題3問を確認。計5問すべてをガイドに記載した |
| 各演習の目的がスライドの説明と矛盾していないか | OK | 例題1は「論理的エラーの発見と解決」、例題2は「繰り返しと条件分岐」の記述と整合 |
| 手順がスライドの指示と整合しているか | OK | 例題1の手順は操作手順と一致。例題2の手順は実行例と整合 |
| 参照スライド番号が正しいか | OK | 各演習の参照番号を該当スライドと照合し、内容の対応を確認した |
| 専門用語の説明がスライドの定義と一致しているか | OK | 書式指定子、while(1)の意味、break文の動作についての説明と一致 |
改善の余地
課題2の数式「Σk(k+1)」について、PDF変換時に数式が正しく表示されていない可能性がある。元の数式の意図を受講者に明確に伝えるため、スライドの修正または補足説明の追加が望ましい。
ce-6. ファイル,配列
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
演習1:テキストファイルからのデータ読み込み(例題1)
| 目的 | テキストファイル(人間が目で読める形式のファイル)からデータを読み込み、必要な列だけを抽出して表示する方法を習得する。ファイル操作の基本であるオープン・読み込み・クローズの流れを理解する。 |
|---|
実施手順
- データファイルの準備を行う。メモ帳を起動し、3行の名簿データを入力する。半角空白でデータを区切ること。全角空白が混入するとプログラムが正常に動作しない点に注意する。
- ファイルを「d:\Book1.txt」として保存する。保存先のドライブは環境に応じて変更してよい。
- プログラムコードをエディタに入力する。ファイルパスは手順2で保存した場所に合わせて修正する。
- プログラムをビルドする。「ビルド」メニューから実行し、「1. 正常終了」のメッセージを確認する。
- プログラムを実行する。「デバッグ」メニューから実行し、実行ウインドウに氏名と住所が表示されることを確認する。
- 以下の点について自分で確認する。fopen関数でNULLが返される条件は何か。fgets関数でNULLが返される条件は何か。
ヒント
ファイルパスの区切り文字について
Cプログラム内では円記号を2つ重ねて「\\」と記述する。これはエスケープシーケンス(特殊な意味を持つ文字の組み合わせ)の規則による。
fgets関数の第2引数について
fgets関数の第2引数「100」は、読み込む最大バイト数を指定している。配列サイズが100の場合、改行文字と終端文字を考慮すると、実際に読み込めるデータ本体は最大98文字である。
sscanf_s関数の動作について
sscanf_s関数は、文字列から書式に従ってデータを取り出す関数である。本演習では、fgetsで読み込んだ1行分の文字列(line)から、空白文字で区切られた3つの文字列を取り出し、それぞれname、birth、addressに格納している。書式指定子「%s」は空白文字(スペース、タブ、改行)を区切りとして文字列を読み取る。
ファイルが開けない場合
ファイルが存在しない、またはパスが間違っている場合、fopen関数はNULLを返す。プログラムが何も表示せずに終了する場合は、ファイルパスを再確認すること。
演習2:ベクトルの内積(例題2)
| 目的 | 一次元配列(0から始まる番号がついたデータの並び)を使用してベクトルの内積を計算する方法を理解する。for文と配列の添字を組み合わせた処理パターンを習得する。 |
|---|
実施手順
- プログラムの構造を読み、以下の要素を確認する。配列uとvの宣言と初期化の方法、for文の繰り返し条件、配列要素へのアクセス方法(u[i]、v[i])。
- トレース図(i=0, 1, 2 での変数の変化)を追い、ipの値がどのように累積されるか確認する。
- プログラムを実際に入力して実行する。
- 実行結果として表示される内積の値を確認する。手計算で 1.9×4.6 + 2.8×5.5 + 3.7×6.4 を計算し、結果が一致することを確かめる。
- 配列のサイズを変更した場合、プログラムのどの部分を修正する必要があるか考察する。
ヒント
double型について
double型は浮動小数点数(小数を含む数値)を扱うためのデータ型である。int型では小数点以下が切り捨てられるため、内積計算には適さない。
配列の添字について
配列の添字は0から始まる。サイズ3の配列の有効な添字は0, 1, 2である。a[3]にアクセスしようとすると配列の範囲外アクセスとなり、予期しない動作を引き起こす可能性がある。
トレースの練習
表を参照し、繰り返しの各回でiの値、条件式の判定、ipの値がどう変化するかを追跡する練習をすると理解が深まる。
演習3:棒グラフを描く(例題3)
| 目的 | 多重ループ(ループの入れ子構造)を使用して、配列データを視覚的に表現する方法を習得する。外側ループと内側ループの役割の違いを理解する。 |
|---|
実施手順
- プログラムを入力する。
- プログラムをビルドし、実行する。棒グラフが表示されることを確認する。
- プログラムの動作を以下の観点で分析する。外側のfor文(変数i)は何を制御しているか。内側のfor文(変数j)は何を制御しているか。a[i]の値が内側ループの繰り返し回数にどう影響するか。
- 多重ループの説明図を参照し、i=0のとき内側ループがa[0]=6回実行され「*」が6個表示される過程を追跡する。
- 配列aの値を変更して再実行し、棒グラフの表示が変化することを確認する。
ヒント
多重ループの動作
多重ループでは、外側ループが1回進むごとに内側ループが最初から最後まで実行される。この演習では、外側ループが「行」を制御し、内側ループが「各行に表示するアスタリスクの個数」を制御している。
改行のタイミング
printf("*")は改行を含まないため、内側ループ内で呼び出すと同じ行にアスタリスクが連続して表示される。printf("\n")で改行が行われ、次の行の描画に移る。
配列サイズの自動決定
配列の宣言時に要素数を省略し、初期化子の数から自動的にサイズが決まる書き方(int a[]={6,4,7,1,5,3,2};)を使用している。この配列のサイズは7である。
ce-7. メモリ内でのデータの配置
資料(スライド): [PDF], [パワーポイント], [HTML]
- Mandrill.bmp [Windows BMP] 実習用ファイル (24ビット Windows ビットマップ)
- Girl.bmp [Windows BMP] 出席課題用ファイル (24ビット Windows ビットマップ)
演習パート(クリックして展開)
演習1:棒グラフ描画プログラム
目的
補助関数を用いたプログラム構造と、配列データを用いた繰り返し処理の動作を理解する。関数呼び出し時のプログラム実行順序を確認する。
手順
- ソースコードを開発環境に入力する
- プログラムをビルド(コンパイル・リンク)する
- 実行し、実行結果例と同様の棒グラフが表示されることを確認する
- 配列aの値を変更して再実行し、棒グラフの表示がどう変わるか観察する
- main関数からdraw_bar関数への制御の流れを追跡する
ヒント
プログラム実行はmain関数から開始される。draw_bar関数が呼び出されると、処理は関数の先頭にジャンプし、return文で呼び出し元に戻る。main関数内の変数iと、draw_bar関数内の変数iは別々のメモリエリアに確保される点に注意する。
演習2:変数のメモリアドレス表示
目的
変数にはメモリアドレス(memory address:変数が格納されているメモリ上の位置を示す値)が割り当てられていることを理解する。アドレス取得演算子「&」と書式指定子「%p」の使い方を習得する。
手順
- ソースコードを開発環境に入力する
- プログラムをビルドする
- 実行し、面積の計算結果と各変数のメモリアドレスが表示されることを確認する
- 表示されたメモリアドレスが例と異なっていても、それは正常な動作であることを理解する
- 模式図と実際のメモリ配置を照らし合わせ、double型変数が8バイトを占めることを確認する
ヒント
メモリアドレスの値は実行環境や実行タイミングによって異なる。表示される具体的な値が例と一致しなくても、プログラムの動作自体は正しい。「&変数名」でメモリアドレスを取得し、printf文の「%p」で16進数形式で表示する。
演習3:配列のメモリアドレス
目的
配列の各要素がメモリ上で連続して配置されることを理解する。配列要素のメモリアドレスを取得・表示する方法を習得する。
手順
- ソースコードを開発環境に入力する
- プログラムをビルドする
- 実行し、内積の計算結果と配列u、vの各要素のメモリアドレスが表示されることを確認する
- 表示されたメモリアドレスを観察し、u[0]、u[1]、u[2]のアドレスが8バイトずつ増加していることを確認する
- 模式図と照合し、配列がメモリ上でどのように配置されているか理解する
ヒント
配列要素のアドレス取得には「&u[0]」のように配列名と添字を指定する。double型は8バイトであるため、連続する要素のアドレスは8ずつ異なる。図に示されるように、配列の論理的な並び(添字0, 1, 2の順)がそのままメモリ上の配置に対応している。
ce-8. 構造体,レコードデータファイル
資料(スライド): [PDF], [パワーポイント], [HTML]
- PersonData.bin [バイナリファイル形式] 実習用ファイル
- Mandrill.bmp [Windows BMP] 実習用ファイル (24ビット Windows ビットマップ)
演習パート(クリックして展開)
演習1:バイナリファイル形式のファイルからのデータ読み込み
目的
構造体(struct:複数のデータをひとまとめにして扱うデータ型)を用いて,バイナリファイル(テキスト形式ではなくメモリ上のデータ表現をそのまま保存したファイル)から名簿データを読み込み,画面に表示する方法を学ぶ。
手順
- ソースコードを開発環境に入力する
- 構造体Personの定義部分を確認し,name(20文字),age(整数),address(20文字)の3つのメンバで構成されていることを理解する
- データファイル「z:\PersonData.bin」を事前に用意する(教員の指示に従う)
- 「ビルド」→「○○のビルド」を選択し,「1.正常終了」のメッセージを確認する
- 「デバッグ」→「実行」を選択してプログラムを実行する
- 実行ウインドウに名前,年齢,住所が表示されることを確認する
- fread関数が構造体1つ分のデータを読み込む動作を,図で確認する
ヒント
fread関数の引数は「読み込み先アドレス,1要素のサイズ,要素数,ファイルポインタ」の順である。sizeof(struct Person)により構造体1つ分のバイト数(44バイト:char 20バイト + int 4バイト + char 20バイト)を取得している。fopen関数でファイルを開く際,戻り値がNULLの場合はファイルが存在しないか,パスが間違っている可能性がある。
演習2:構造体データのメモリ配置を見る
目的
構造体の配列がメモリ上でどのように配置されるかを,アドレス表示により確認する。double型(倍精度浮動小数点数:8バイト)のメンバを持つ構造体のサイズを理解する。
手順
- ソースコードを開発環境に入力する
- 構造体ImaginaryNumber(複素数を表す構造体)の定義を確認する。real_part(実部)とimaginary_part(虚部)の2つのdouble型メンバで構成される
- 配列aの宣言と同時に初期化している部分を確認する。{{3.1, -2.4}, {4.5, 5.1}, {-2.4, 6.3}}の形式で3つの複素数を設定している
- プログラムをビルドし,実行する
- 表示された複素数の値とメモリアドレスを確認する
- 連続するメモリアドレスの差が16バイト(double 8バイト × 2メンバ)であることを計算して確認する
ヒント
printf関数の書式指定子「%p」はメモリアドレスを16進数で表示する。「&」演算子は変数のメモリアドレスを取得する。メモリアドレスの具体的な値は実行環境により異なるが,連続する要素間の差(16バイト)は一定である。図と自分の実行結果を比較し,構造を理解すること。
演習3:バイナリファイル形式のファイル読み出し,書き込み(参考)
目的
fwrite関数によるバイナリファイルへの書き込みと,fread関数による読み出しの両方を1つのプログラムで行う流れを理解する。
手順
- ソースコードを確認する
- 構造体配列origに3人分のデータが初期化されている部分を確認する
- fwrite関数でorig配列のデータをファイルに書き込む処理の流れを追う
- fread関数で配列aにデータを読み込む処理の流れを追う
- プログラムをビルドし,実行する
- 書き込んだデータと読み込んだデータが一致することを確認する
ヒント
fwrite関数の引数構成はfreadと同様で「書き込み元アドレス,1要素のサイズ,要素数,ファイルポインタ」である。書き込みモードは"w",読み込みモードは"r"をfopenに指定する。この演習は参考用の見本として提示されており,書き込みと読み出しの対応関係を理解することが目的である。
演習4:Windowsビットマップファイルの読み出し,書き込み
目的
24ビットカラーのWindowsビットマップファイル(画像ファイル形式の一種)を構造体で読み込み,画素データに対して横方向差分処理を行う方法を学ぶ。
手順
- 24ビットカラーのビットマップファイル(例:z:\Mandrill.bmp)を用意する
- ビットマップファイルの構造を確認する。ファイルヘッダ(BITMAPFILEHEADER),ヘッダ(BITMAPINFOHEADER),本体(画素データ)の3部構成である
- 差分計算式を理解する。x > 0の場合は隣接画素との差の絶対値を計算し,x = 0の場合は0とする
- freadを3回実行してファイルヘッダ,ヘッダ,本体を順に読み込む処理を確認する
- 画素データに対して差分計算を行う
- 処理結果をfwriteで新しいファイル(例:z:\done.bmp)に書き込む
- 出力ファイルを画像ビューアで開き,エッジ(輪郭)が強調された画像になっていることを確認する
ヒント
画素(x, y)のデータは,画像本体の先頭から3×(x + y×幅)バイト目に位置する。各画素は青(B),緑(G),赤(R)の順に各1バイトで格納されている。差分処理では絶対値を取るため,結果は0〜255の範囲に収まる。構造体BITMAPFILEHEADERとBITMAPINFOHEADERの定義を参照すること。
ce-9. ポインタ,連結リスト
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
リストの長さを求める関数の作成
目的
連結リスト(linked list:データ部分とポインタ部分で構成されるリストセルを連ねたデータ構造)を先頭から末端まで走査し、要素数を数える関数を自作することで、リストの走査処理を理解する。
手順
- 構造体定義(struct data_list)を確認し、リストセルがdataメンバとnextメンバで構成されていることを把握する。
- print_list関数を参照し、リストを先頭から末端まで走査するパターンを確認する。走査の基本形は以下のとおりである。
- ポインタ変数xを先頭セルのアドレスで初期化する
- xがNULLでない間、whileループを繰り返す
- ループ内で
x = x->nextにより次のセルへ移動する
- 図を順に追い、lengthの実行状況を視覚的に理解する。xが各セルを順に指し、最終的に4という結果が得られることを確認する。
- length関数を以下の仕様で作成する。
- 関数名:length_list(または任意の名前)
- 引数:struct data_list *p(リストの先頭を指すポインタ)
- 戻り値:int型(リストの要素数)
- プログラムをベースに、作成したlength_list関数を追加する。
- main関数内で、リスト生成後にlength_list関数を呼び出し、結果をprintfで表示するコードを追記する。
- プログラムをコンパイル・実行し、期待される結果(要素数4)が出力されることを確認する。
ヒント
考察のポイントとして、print_list関数では最初の要素を特別扱いしているが、length関数では全要素を同等に扱えるため、より単純な実装が可能である。
よくある間違いの回避方法として、以下の点に注意する。
- カウンタ変数の初期化を忘れないこと。走査開始前に0で初期化する。
- リストが空(p == NULL)の場合の処理を考慮すること。空リストの長さは0である。
- ループ内でカウンタをインクリメントするタイミングと、次のセルへ移動するタイミングを混同しないこと。
find_list関数も参考になる。find_listでは条件一致時に1を返すが、length関数では走査しながらカウンタを増やし、最終的にカウンタの値を返す。
ce-10. 二分探索木
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
例題1:リストの併合
目的
2つの連結リストを併合する操作を通じて、tail(末尾)ポインタの有用性とリスト操作の基本を理解する。
手順
- プログラムコードをテキストエディタに入力し、ソースファイルとして保存する
head1とtail1、head2とtail2がそれぞれどのリスト要素を指しているか、コードを読みながら紙に図示する- プログラムをコンパイルし、実行する
- 「併合前」と「併合後」の出力結果を確認する
- コード中の
tail1->next = head2; tail1 = tail2;という2行の役割を考察する insert_list関数が要素をリストの先頭に挿入する仕組みを追跡する
ヒント
tail1は常にリストの末尾を指している。併合時にtail1->nextにhead2を代入することで、リスト1の末尾とリスト2の先頭が接続される。tail1 = tail2は併合後の新しい末尾を更新している。ポインタの指す先を図に描きながら追跡すると理解しやすい。
例題2:二分探索木の生成
目的
new_node関数を用いて二分探索木(binary search tree:整列可能なデータ集合に対し効率的な探索を行うためのデータ構造)を生成し、中間順(in-order)走査で整列済みデータを出力する方法を理解する。
手順
- 二分探索木のノード構造体
BTNodeとnew_node関数の定義を確認する - プログラムコードをテキストエディタに入力し、保存する
new_node関数の入れ子呼び出しによって、どのような木構造が生成されるか、図を参照しながら紙に描く- プログラムをコンパイルし、実行する
- 出力結果が昇順(13, 21, 35, 40, 46, 61, 69)になっていることを確認する
print_data関数の再帰呼び出しの順序を追跡し、なぜ昇順に出力されるのかを考察する
ヒント
二分探索木では「左側の子は親より小さく、右側の子は親より大きい」という規則がある。in-order走査は「左部分木→自分→右部分木」の順に処理するため、この規則と組み合わせると昇順出力となる。再帰呼び出しの流れを追跡する際は、呼び出しスタック(関数呼び出しの積み重なり)を紙に書き出すと分かりやすい。
例題3:二分探索木における探索
目的
二分探索木における探索アルゴリズムを実装し、探索キーと各節点(node:木構造を構成する要素)のデータを比較しながら目標データを見つける手順を理解する。
手順
- 探索アルゴリズムの原理を確認する
- プログラムコードをテキストエディタに入力し、保存する
- プログラムをコンパイルし、実行する
- 「40 は有り」と出力されることを確認する
find_node関数の呼び出し部分をfind_node( 41, root )に変更し、再コンパイル・実行する- 「41」を探索する場合にどの経路をたどるか、木構造図を使って追跡する
find_node関数内のwhileループが終了する条件を確認する
ヒント
探索キーが節点のデータより大きければ右の子へ、小さければ左の子へ進む。存在しない要素を探索した場合、最終的にnodeがNULLになりループを抜ける。「節点40を探す」例を参考に、自分で別の値(例:13、69、50)を探索する経路を追跡してみるとよい。
例題4:二分探索木への挿入
目的
二分探索木に新たな要素を挿入する関数を作成し、再帰的なアルゴリズムによって適切な位置にデータを配置する方法を理解する。
手順
- 挿入アルゴリズムの原理を確認する
- プログラムコードをテキストエディタに入力し、保存する
- プログラムをコンパイルし、実行する
- 出力結果が例題2と同じ昇順(13, 21, 35, 40, 46, 61, 69)になることを確認する
insert_node関数の再帰呼び出しを追跡し、例えば「21」がどのようにして「35」の左の子として配置されるかを考察する- 挿入順序を変えた場合(例:13, 21, 35, 40, 46, 61, 69の順)にどのような木構造ができるか考察する
ヒント
insert_node関数は再帰的に定義されており、挿入位置(node == NULLとなる場所)を見つけるまで左または右の部分木をたどる。同じデータ集合でも挿入順序によって木の形状が変わる点に注意する。最初に最小値を挿入し続けると、右側にのみ子が連なる偏った木になる。
補足:二分木の走査法
二分木の走査法(traversal:木構造の全ノードを一定の順序で訪問する方法)として以下の3種類がある。
| 走査法 | 順序 |
|---|---|
| 先行順(pre-order) | 根ノード→左部分木→右部分木 |
| 中間順(in-order) | 左部分木→根ノード→右部分木 |
| 後行順(post-order) | 左部分木→右部分木→根ノード |
例題2〜4で使用しているprint_data関数は中間順走査を実装したものである。他の走査法を実装する場合は、printf文の位置を変更することで実現できる。
ce-11. 中間まとめ2
資料(スライド): [PDF], [パワーポイント], [HTML]
- Address.txt [テキストファイル形式] 実習用ファイル
演習パート(クリックして展開)
演習1:プログラム全体の入力と実行
| 目的 | 名簿ファイルを読み込み、氏名順にソートして表示するプログラムの全体構造を理解する。 |
|---|---|
| 手順 |
|
| ヒント |
ファイルパス「z:\\Address.txt」は環境に合わせて変更が必要な場合がある。ファイルオープンに失敗すると何も表示されずに終了するため、ファイルパスと形式を再確認すること。
注意(strcpy_s関数について):strcpy_s関数はC11規格のオプション機能(Annex K)であり、Visual Studioでは標準で使用可能だが、GCCやClangなど他のコンパイラでは使用できない場合がある。その場合はstrcpy関数に置き換えるか、コンパイラの設定を確認すること。
|
演習2:構造体Person と構造体BTNode の理解
| 目的 | 名簿データを格納するPerson構造体と、二分探索木のノードを表すBTNode構造体の役割と関係を理解する。 |
|---|---|
| 手順 |
|
| ヒント |
BTNodeはポインタ(left、right)を通じて他のBTNodeを参照する。これにより、動的に木構造を構築できる。Person構造体の内容は格納すべきデータに応じて変更可能である点に注意すること。
|
演習3:ファイル読み込みと二分探索木の構築
| 目的 | read_file_and_create_tree関数がファイルからデータを読み込み、二分探索木を構築する過程を理解する。 |
|---|---|
| 手順 |
|
| ヒント |
fgetsはファイルの終端でNULLを返すため、whileループの終了条件となる。sscanf_sの書式指定子「%s %d/%d/%d %s %s」がファイル形式と対応していることを確認すること。
|
演習4:二分探索木への挿入処理の理解
| 目的 | insert_person_node関数の再帰的な挿入アルゴリズムを理解し、氏名をキーとした二分探索木(Binary Search Tree: 各ノードにおいて左部分木のすべての値が親より小さく、右部分木のすべての値が親より大きい木構造)の性質を把握する。 |
|---|---|
| 手順 |
|
| ヒント |
strcmp(a, b)は、aがbより辞書順で前なら負の値を返す。この性質により、氏名の辞書順で二分探索木が構成される。重複データ(同名)は挿入されない点に注意すること。
|
演習5:通りがけ順(in-order traversal)による表示
| 目的 | 通りがけ順(in-order traversal: 左部分木→根→右部分木の順で走査する方法)により、二分探索木のデータが昇順(辞書順)で出力される仕組みを理解する。 |
|---|---|
| 手順 |
|
| ヒント |
二分探索木を通りがけ順で走査すると、キーの昇順(この場合は氏名の辞書順)でデータが得られる。これが「ソートして表示」を実現する原理である。再帰呼び出しの順序(左→出力→右)がこの性質を生み出している。
|
ce-12. ニュートン法による方程式の求解,台形則による数値積分
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
演習:ニュートン法のプログラム(例題1)
目的
ニュートン法(Newton's method:非線形方程式の近似解を反復計算で求める数値解法)をCプログラムとして実装し、反復公式による収束過程を観察する。
手順
- ニュートン法の原理を理解する。特に反復公式 xi+1 = xi − f(xi) / f'(xi) の意味を確認する。
- 対象とする関数 f(x) = x² − 2 について、導関数 f'(x) = 2x を手計算で求める。
- ソースコードを参考に、以下の構成でプログラムを作成する。
- 関数 f(x):f(x) = x² − 2 を返す
- 関数 g(x):導関数 f'(x) = 2x を返す
- main関数:初期値 x₀ = 10、収束判定値 δ = 0.000001 を設定し、反復計算を行う
- 反復公式
new_x = x - f(x) / g(x)を用いて、各反復で new_x、f(x)、g(x) の値を出力する。 - 収束判定として |f(new_x)| < δ となった時点で計算を終了し、解を出力する。
- プログラムをコンパイル・実行し、実行結果と比較する。
- 初期値 x₀ を変更して(例:x₀ = −10、x₀ = 0.1)実行し、収束回数や得られる解がどのように変化するか観察する。
考察のポイント
f(x) = x² − 2 = 0 の解は x = ±√2 ≈ ±1.414... である。初期値 x₀ = 10 から出発した場合、正の解 √2 に収束することを確認する。
初期値を負の値(例:x₀ = −10)にすると、負の解 −√2 に収束する。これは「x₀の値によって求まる解が変わる」性質である。
よくある間違いの回避方法
初期値 x₀ = 0 を設定すると g(0) = 2 × 0 = 0 となり、ゼロ除算が発生する。f'(x) = 0 となる点を選ぶと計算が破綻するため、初期値の選択には注意が必要である。
fabs関数(絶対値を求める関数)を使用するには #include <math.h> が必要である。
ce-13. 計算精度と誤差
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
演習1:整数除算の結果を確認する
目的
整数同士の除算では小数点以下が切り捨てられることを、実際のプログラム実行を通じて理解する。
手順
- プログラムコードをテキストエディタに入力する
- コンパイルして実行ファイルを生成する
- プログラムを実行し、出力結果を確認する
- 変数rの型がdoubleであるにもかかわらず、結果が0.000000となる理由を考察する
ヒント
右辺の「1 / 2」は整数リテラル(literal:プログラム中に直接記述された値)同士の演算である。C言語では、演算に関わるすべての値が整数の場合、結果も整数として計算される。代入先の型がdoubleであっても、計算が完了した後に代入されるため、切り捨て後の0が代入される。
演習2:浮動小数点数除算との比較
目的
浮動小数点数リテラルを用いた除算では小数点以下の値が保持されることを確認し、演習1との違いを理解する。
手順
- プログラムコードを入力する
- コンパイルして実行し、出力結果を確認する
- 演習1の結果(0.000000)と演習2の結果(0.500000)を比較する
- 「1 / 2」と「1.0 / 2.0」の違いを整理する
ヒント
1.0や2.0のように小数点を含む数値リテラルは浮動小数点数として扱われる。片方でも浮動小数点数が含まれる演算では、もう一方も浮動小数点数に変換されてから計算が行われる(暗黙の型変換)。
演習3:丸め誤差の集積(情報落ち)を観察する
目的
浮動小数点数の丸め誤差(rounding error:有限桁数で表現するための近似による誤差)が繰り返し計算で集積する現象を観察する。
手順
- プログラムコードを入力する
- コンパイルして実行する
- 出力結果が期待値(20000)と異なることを確認する
- 0.0001を1億回加算した結果がなぜ20000にならないかを、2進数表現の説明を参照して考察する
ヒント
10進数の0.1は2進数では無限小数となり、有限のビット数では正確に表現できない。このため、0.0001のような値を繰り返し加算すると、各回の微小な誤差が蓄積し、最終結果に無視できない誤差が生じる。ループ制御に浮動小数点数を使用する場合は、この現象に注意が必要である。
演習4:2次方程式の解を求めるプログラムの検証
目的
2次方程式の解の公式を実装したプログラムを実行し、係数の値によって計算精度が変化することを観察する。
手順
- プログラムコードを入力する(sqrt関数を使用するため、math.hのインクルードが必要)
- コンパイルして実行する
- 以下の係数セットを順に入力し、結果を記録する
- (a, b, c) = (1, -2, 1)
- (a, b, c) = (1, -1000, 1)
- (a, b, c) = (1, -1000000, 1)
- (a, b, c) = (1, -100000000, 1)
- 各結果について、解x1, x2を方程式の左辺に代入した値(括弧内の数値)を観察する
- bの絶対値が大きくなるにつれて、x2の検算結果が0から離れていく傾向を確認する
ヒント
解の公式で「-b ± √D」を計算する際、|b|と√Dが非常に近い値になると、減算の結果として有効数字が大きく減少する。これが桁落ち(cancellation error)である。x2の左辺の値が0から離れていく現象はこの桁落ちに起因する。
演習5:桁落ちを回避する改良版プログラムの検証
目的
桁落ちを回避するアルゴリズムを実装し、演習4の結果と比較することで、数値計算における式の変形の重要性を理解する。
手順
- 改良版プログラムコードを入力する
- コンパイルして実行する
- 演習4と同じ係数セットを入力し、結果を記録する
- 演習4の結果と比較し、特にbの絶対値が大きい場合のx2の精度改善を確認する
- 改良の仕組み(|b|+√Dのみを使用、解と係数の関係の利用)を理解する
ヒント
改良版では、bの符号に応じて加算のみを用いる形式に変形することで、近い値同士の減算を回避している。もう一方の解は解と係数の関係(x1 × x2 = c/a)から算出するため、桁落ちが発生しない。このように、数学的には等価な式であっても、数値計算では計算順序や式の形式によって精度が大きく異なる場合がある。
ce-14. 行列,線形方程式
資料(スライド): [PDF], [パワーポイント], [HTML]
演習パート(クリックして展開)
演習1:LU分解プログラムの実行と動作確認
目的
LU分解を用いた連立一次方程式の解法を,Cプログラムの実行を通じて理解する。
手順
- データファイル
c:\lu_data.txtを作成する。ファイルには以下の内容を記述する。3 2 6 1 17 2 4 1 6 23 5 4 1 3 23 3 2 5 6 26 - プログラムコードをテキストエディタに入力し,ソースファイルとして保存する。
- プログラムをコンパイルし,実行ファイルを生成する。
- プログラムを実行し,実行結果と自身の結果を比較する。
- 出力されたL行列とU行列について,計算式と照合し,各要素がどのように計算されたか追跡する。
- 出力された解 x0, x1, x2, x3 を元の連立一次方程式に代入し,等式が成立することを確認する。
ヒント
- ファイルパス
c:\lu_data.txtは環境に応じて変更が必要な場合がある。LinuxやmacOSの場合はパス形式が異なる。 #pragma warning(disable:4996)はVisual Studio向けの警告抑制であり,他のコンパイラでは不要または別の対処が必要となる。- L行列の対角成分はすべて1であること,U行列の対角成分より下はすべて0であることを確認する。
演習2:LU分解できない場合の動作検証
目的
対角要素が0となる場合にLU分解が失敗する原因を理解する。
手順
- 以下のデータでファイルを作成する。
0 3 4 1 20 3 2 2 2 19 5 6 2 3 27 4 8 5 2 14 - 演習1のプログラムを実行する。
- プログラムの動作結果を観察する。エラーが発生するか,不正な値が出力されるかを記録する。
- コードにおいて,0除算が発生する箇所を特定する。具体的には
l[j][i] /= u[i][i];の行に着目する。 - なぜ最初の行の最初の要素が0であると問題が生じるのか,計算式を参照して考察する。
ヒント
- LU分解では u0,0 = A0,0 となる。A0,0が0の場合,l1,0 = A1,0/u0,0 の計算で0除算が発生する。
- プログラムは0除算に対するエラー処理を含んでいないため,実行環境によって動作が異なる場合がある。
演習3:ピボッティングの実装検討
目的
ピボッティング(pivoting:行の入れ替えによって数値計算の安定性を向上させる手法)の概念を理解し,LU分解の数値的安定性を向上させる方法を検討する。
手順
- ピボッティングの基本概念を確認する。行の入れ替えによって0除算を回避できることを理解する。
- 絶対値が小さい値で除算した場合に丸め誤差(計算機における浮動小数点演算の誤差)が大きくなる理由を考察する。
- 演習1のプログラムに対して,以下の機能を追加する方法を検討する(実装は任意)。
- LU分解の各ステップで,対角要素が0または0に近いかを判定する
- 必要に応じて行を入れ替える
- 行を入れ替えた場合,最終的な解にどのような影響があるかを考察する。
ヒント
- 部分ピボッティング(partial pivoting)では,現在の列において絶対値が最大の要素を持つ行を選択する。
- 行の入れ替えは,係数行列Aと右辺ベクトルbの両方に対して行う必要がある。
- ピボッティングは解の精度向上だけでなく,計算の安定性にも寄与する。
演習4:解が求まらない場合の検証
目的
連立一次方程式に解が存在しない,または一意に定まらない場合の条件を理解する。
手順
- 以下の連立一次方程式を観察する。
x0 + 2x1 + 2x2 + 2x3 = 19 x0 + 2x1 + 2x2 + 2x3 = 17 x0 + x1 + x2 + 2x3 = 14 x1 + x2 + x3 = 10 - 第1式と第2式を比較し,なぜこの連立一次方程式が解を持たないか考察する。
- 3元連立一次方程式について,4つの未知数に対して3つの式しかない場合,解がどうなるか考察する。
- 行列のランク(階数:行列の線形独立な行または列の最大数)の概念と,解の存在条件との関係について調査する。
ヒント
- 左辺が同一で右辺が異なる2つの式が存在する場合,その連立一次方程式は矛盾を含み,解が存在しない(不能)。
- 式の数が未知数の数より少ない場合,解が一意に定まらない(不定)。
- 行列の観点では,係数行列のランクと拡大係数行列のランクの関係が解の存在・一意性を決定する。