2025年6月
1234567
891011121314
15161718192021
22232425262728
2930  

検索・リンク

型の無い言語からC言語を学ぶと

情報構造論の中間試験の採点をしていて気づいたこと。

プログラムが解っていない学生の回答で、関数呼び出しで実引数に int とか実引数の型を書く学生がいる。

int foo( int x ) {
   return x * x ;
}
void main() {
   for( int i = 0 ; i < 10 ; i++ )
      printf( "%d" , foo( int i ) ) ;
}                         ~~~ なぜ int を書くの?

でも、最近の学生はプログラミングで最初に習うプログラム言語が、JavaScript だったり、Python だったりと、型の無い言語であることが多い。

こういう学生さんは、関数宣言と関数呼び出しでは、以下のような感じで習うことだろう。

// JavaScript
function foo( x ) {
   return x * x ;
}
for( var i = 0 ; i < 10 ; i++ )
   print( foo( i ) ) ;

## Python
def foo( x )
   return x * x

for i in range( 0 , 10 )
   print( foo( i ) )

んで、こういう学生さんは、関数宣言の仮引数部分を見ると、変数名しか書いてない。この後にC言語を習うと、仮引数の宣言で foo( int x ) みたいに書いてあると、そこに int と書かないとダメみたいに思う、もしくは int x で一つの引数みたいに思うんだろうな。
だから、関数呼び出しの実引数にで、foo( int i ) みたいに型名を書いてしまうと思われる。int というのは、整数型にするためのもの…みたいなイメージなんだろうか?。

C言語での関数宣言と関数呼び出しの説明

// fooという関数の宣言
int foo( int x )    // foo()の関数の答えの型は、int型。
         ~~~~~~仮引数
{                   // 仮引数 x の型は、int 型。
   return x * x ;
}
void main() {
   for( int i = 0 ; i < 10 ; i++ )
      printf( "%d¥n" , foo(  i  ) ) ;
                            ~~~実引数
      // foo() の第一引数は、整数型を書かないといけない。
      // 1番目の実引数 i の型は、int i で宣言されているから、int 型。
      // foo( 1.2 ) と書くと、第一引数は double だから、
      // 引数の型が合わないのでエラー。
}

関数宣言の引数は、仮引数と呼ぶ。関数を呼び出すときの引数は、実引数と呼ぶ。
C言語では、関数宣言の仮引数には「型」を明示する必要がある。(というか変数宣言では型を明示しないとダメ)
さらにC言語では関数を呼び出すときに、仮引数と実引数で「型」が一致していないとエラーとなる。

JavaScript や Python では、変数に「型」という概念が不要なので、仮引数宣言には「型」を明示する必要がない。

C言語 や Java は、静的な型付けの言語。最初から型がきまっていないとダメな言語

これに対し、JavaScript や Python という言語は、動的な型付けの言語。プログラム言語が実行中に型を意識しながら動く言語。

福井高専の対外接続の速度

福井高専ではSINETによって接続されているけど、次の更新の際に十分な性能があるかのために、ネットワーク利用状況の確認。

瞬間最大風速で、150Mbps 。ムーアの法則に従うとすれば、2年で2倍と考えると、2022年で約500Mbps, 2025年で1.2Gbps 程度となる。特に今後 学内でBYODが拡大すると、さらなる通信速度が必要と考えられる。

MoodleのSTACKをインストール

数学の先生より、本校で運用している Moodle サーバで、数学の表示や小テストのためのプラグインの STACK をインストールしてほしいとの要請があったので、インストールしてみた。

maxima,gnuplotなどのインストール

STACK は、数式表示に maxima 、グラフ表示に gnuplot を使うのでインストール

# aptitude install maxima
# aptitude install gnuplot gnuplot-common

mathjaxのインストール

JavaScript で 数式表示をするための mathjax をインストール

# aptitude install libjs-mathjax fonts-mathjax-extras

プログラムは /usr/share/javascript/mathjax/ に保存されているので moodle から使えるように、ライブラリに登録

# cd /usr/share/moodle/lib
# ln -sf /usr/share/javascript/mathjax .

STACKに必要なmoodleプラグインのインストール

“moodle stack”でググったサイトの情報では、zip で拾ってきて、解凍、リネームとか書いてあったけど面倒なので、他のやり方として git の説明があったので、そのまま git を実行

# cd /usr/share/moodle
# git clone git://github.com/maths/
                  moodle-qbehaviour_dfexplicitvaildate.git
            question/behaviour/dfexplicitvaildate
# git clone git://github.com/maths/
                  moodle-qbehaviour_dfcbmexplicitvaildate.git
            question/behaviour/dfcbmexplicitvaildate
# git clone git://github.com/maths/
                  moodle-qbehaviour_adaptivemultipart.git
            question/behaviour/adaptivemultipart

一旦、管理者アカウントでログインし、管理ブロックから通知をクリックしてインストールを完了させる。

STACK本体のインストール

さらに、stack 本体をインストール

# git clone git://github.com/maths/quiz_stack.git
            mod/quiz/report/stack

管理ブロックから通知をクリックしてインストールを完了。

# git clone git://github.com/maths/moodle-qformat_stack.git
            question/format/stack

再び、管理ブロックから通知をクリックしてインストールを完了。

最後に動作確認で、無事数式やグラフが表示されていたので、大丈夫だろう。

遅すぎるのでCASのキャッシング

インストールの参考にしたサイトの記事では、数式やグラフ表示などで CAS を使うけど、大量の情報が残ってHDDを消費するとの記載が書いてあったので、キャッシングしない設定にしていた。しかし、遅すぎて使い物にならないのでは意味がないので、「データベースのキャッシュ」にしてみた。

当面、これで運用してみて、HDD が逼迫するようであれば、Moodle サーバはAzureサーバ上なので、HDD の設定を検討してみよう。

TCP/IPのWikipedia記事の輪読

工業英語の高久研・斉藤研の中間試験以後のテーマは、以下の通りとする。

  • 以下に示す Wikipedia 英語版の TCP/IP(Internet Protocol Suite) の記事を、全員で輪読とする。
  • 範囲は最初から、See also の前まで。
  • 担当者は、自分の範囲について記事を和訳し(意訳で良い)、1行目英語2行目和訳した資料を作成し、担当回に配布し、文毎に英語音読,和訳とその説明を行ってもらう。
  • 記事の中の専門用語についても、質問がでたら対応できるように調べ、配布資料に簡単な説明も別途併記すること。(用語調査は日本語情報を調べて良い)
  • 担当回の配布資料は、輪読後に和訳に問題の箇所があったら修正し再提出を行う事。修正箇所は赤字で示す事。和訳の修正がない場合も、修正点がないことを明記して再提出すること。
  • 期間内に輪読が終わった場合は、自分の担当範囲の専門用語から自分の興味のある単語を選び、そのWikipedia英語版の説明記事の概要部分について、上記と同等の資料にてレポートとする。

講義は、斉藤が水曜3限目が授業のため、高久・斉藤グループのみ、水曜3限目卒業研究、木曜3限目工業英語とする。他の授業の都合で入れ替えが出来ない時は、別途指示をします。

今日は、上記資料をダウンロードし大まかに人数分で範囲分けを行い、担当箇所を決めて担当者とその範囲を報告すること。そのあと、自分の担当箇所の和訳作業の時間とする。

Formsによる出席確認

昨年度から学際科目の担当で、出席とるのが大変。

んで最近は、Microsoft Forms のアンケート機能で出席確認している。ただ、今日は授業開始前にすでに出席の解答があって…サボりなのに回答か!?!?ということで、急遽「今日のキーワード」の解答欄を追加した。

そこで、今日のキーワードは「暑いなぁ」と記入して送信してね…と伝えたら「ひらがなですか?漢字ですか?」と聞いてくる。面倒だし「趣旨さえあってりゃいいよ!」といい加減に答えたら….「暑いナ」「暑い」に混ざって「горячий」とか「暑いな\( ‘ω’)/ヒィヤッハァァァァァァァア!!!」とか….しまいにゃ「サザンオールスターズ」という解答が….。

# サザンって、おまえ歳いくつやネン…。

仮想関数を用いた課題

第2回レポート課題

  1. 純粋仮想基底クラスの資料を参考に、複素数データ(直行座標系でも極座標でもよい)の並び替えを行うプログラムを作成せよ。ただし、(1)複素数専用の並び替え関数を作らないこと。(2)複素数用の比較関数を作ること。(3)Object型の並び替え my_sort() を使うこと。
  2. 生物を表す基底を作成し、以下の機能を持つ派生クラスを作成せよ。
    1. 生物クラスは、<名前>を持つ。
    2. 哺乳類クラス(Mammal)に、move() を実行すると、”<名前>は歩く”と表示すること。
    3. 哺乳類クラスに、spawn() を実行すると、”<名前>は子供を産む”と表示すること。
    4. 鳥クラス(Bird)に、move() を実行すると、”<名前>は飛ぶ”と表示すること。
    5. 鳥クラスに、spawn() を実行すると、”<名前>は卵を産む”と表示すること。
    6. 人間クラス(Human)に move() を実行すると、”<名前>は歩く”と表示されること。
    7. 人間クラスに spawn() を実行すると、”<名前>は子供を産む”と表示されること。
    8. にわとりクラス(Chiken)に、move(),spawn() を実行できること。
    9. かものはしクラス(SeaBream)を作るにはどうすればいいか考察せよ。
    class Creature {
    } ;
    class Mammal : ...... {
    } ;
    class Human : ...... {
    } ;
    class Bird : ...... {
    } ;
    class Chiken : ...... {
    } ;
    int main() {
       Mammal tama_cat( "tama" ) ;
       tama_cat.move() ;    // tamaは歩く
       tama_cat.spawn() ;   // tamaは子供を産む
    
       Human  jane_human( "jane" ) ;
       jane_human.move() ;  // janeは歩く
       jane_human.spawn() ; // janeは子供を産む
    
       Bird   tori_bird( "tori" ) ;
       tori_bird.move() ;   // toriは飛ぶ
       tori_bird.spawn() ;  // toriは卵を産む
    
       Chiken piyo_chiken( "PiyoPiyo" ) ;
       piyo_chiken.move() ; // PiyoPiyoは飛ぶ
       piyo_chiken.spawn() ;// PiyoPiyoは卵を産む
    
       SeaBream golduck( "golduck" ) ;
       golduck.move() ;     // golduckは歩く
       golduck.spawn() ;    // golduckは卵を産む
    }
    

D/A・A/D変換回路と誤差

小型コンピュータを使った制御では、外部回路に指定した電圧を出力(D/A変換)したり、外部の電圧を入力(A/D変換)したりすることが多い。以下にその為の回路と動作について説明する。

D/A変換回路

ラダー抵抗回路によるD/A変換の仕組みを引用

このような回路で、D0,D1,D2 は、デジタル値の0=0[V] , 1=5[V] であった場合、Output 部分の電圧は、(D0,D1,D2)の値が、(0,0,0),(0,0,1),…(1,1,1)と変化するにつれ、5/8[V]づつ増え、(1,1,1)で 5*(7/8)=4.4[V]に近づいていく。Output が出力によって電圧が変化しないように、アンプ回路を通す。

DCモータをアナログ量で制御しないこと

このように、電圧をコンピュータから制御するようになると、ロボットで模型用の直流モータの回転速度をこれで制御したい…と考えるかもしれない。
しかし、直流モータは、ブラシとコイル(電磁石)を組み合わせたものだが、モーターが回転しだす瞬間でみれば、コイルは単なる導線である。このため、小さい電流でゆっくりモータを回転させようとすると、たとえ小さい電圧でも導線(抵抗はほぼ0[Ω])には大量の電流が流れ、モータをスイッチングする回路は焼き切れるかもしれない。


PWM変調

こういう場合には、PWM変調(Pulse Width Modulation) を行う。

このような波形であれば、低速度でも電流が流れる時間が短く、大量の電流消費は避けられ、モーターをまわす力も安定する。

A/D変換回路

D/A変換とは逆に、アナログ量をデジタル値に変換するには、どのようにするか?

このような場合には、A/D変換回路を用いる。一般的な回路では、以下のような逐次比較型A/D変換を用いる。

この回路では、変換開始と共に入力値をサンプル保持回路でアナログ量を保存する。
その後、Registerの中のデジタル値を、D/A 変換回路でアナログ量に変換した結果を、比較器(Comparator)でどちらが大きいか判断し、その結果に応じて2分探索法とかハイアンドローの方式のように、比較を繰り返しながらデジタル値を入力値に近づけていく。

ハイアンドロー(数あてゲーム)

数あてゲームで、デタラメな0〜127までの整数を決めて、ヒントを元にその数字を当てる。回答者は、数字を伝えると、決めた数よりHighかLowのヒントをもらえる。
最も速い回答方法は…

例えば決めた数が55だとすると

・初期状態    ???????  0..127
・64 - Low   0??????  0..63
・32 - High  01????? 32..63
・48 - High  011???? 48..63
・56 - Low   0110??? 48..55
・52 - High  01101?? 52..55
・54 - High  011011? 54..55
・55 - Bingo 0110111 55確定

どんな値でも、7回(27=127)までで当てることができる。

量子化と量子化誤差

アナログデータ(連続量)デジタルデータなどの離散的な値で近似的に表すことを、量子化という。

量子化誤差とは、信号をアナログからデジタルに変換する際に生じる誤差のことをいう。

アナログ信号からデジタル信号への変換を行う際、誤差は避けられない。アナログ信号は連続的で無限の正確さを伴うが、デジタル信号の正確さは量子化の解像度やアナログ-デジタル変換回路のビット数に依存する。

偶然誤差

アナログ信号がA/D変換回路に入るまでに、アナログ部品の電気的変動(ノイズ)が原因で値が変動することもある。ノイズが時間的に不規則に発生し、値が増えてしまったり減ってしまったり偶然に発生するものは偶然誤差という。偶然誤差を加えると相殺されてほぼ0になるのであれば、統計的な手法で誤差の影響を減らすことができる

数値と誤差

コンピュータで計算すると、計算結果はすべて正しいと勘違いをしている人も多い。ここで、改めて誤差について考える。
特に、A/D変換したような値であれば、値自体に誤差が含まれている。

こういった誤差が含まれる数字を扱う場合注意が必要である。例えば、12.3 と 12.300 では意味が異なる。測定値であやふやな桁を丸めたのであれば、前者は 12.25〜12.3499 の間の値であり有効数字3桁である。後者は、12.2995〜12.300499 の間の値であり、有効数字5桁である。このため、誤差が含まれる数字の加算・減算・乗算・除算では注意が必要である。

加減乗除算の場合

加減算であれば小数点の位置を揃え、誤差が含まれる桁は有効桁に含めてはいけない。

上記の計算では、0.4567の0.0567の部分は意味がないデータとなる。(情報落ち)

乗除算であれば、有効桁の少ない値と有効桁の多い値の計算では、有効桁の少ない方の誤差が計算結果に出てくるため、通常は、有効桁5桁と2桁の計算であれば、乗除算結果は少ない2桁で書くべきである。

桁落ち

有効桁が大きい結果でも、減算が含まれる場合は注意が必要である。

例えば、以下のような計算では、有効桁7桁どうしでも、計算結果の有効桁は3桁となる。

このような現象は、桁落ちと呼ばれる。

なぜデジタル信号を使うのか

コンピュータが信号処理でなぜ使われるのか?例えば、下の信号のように、電圧の低い/高いで0/1を表現したとする。

このデータ”01011100″を通信相手に送る場合、通信の途中でノイズ(図中の赤)のような信号が加わった場合、アナログ信号では、どれがノイズなのか判別することはできない。しかしデジタル信号であれば、真ん中青線より上/下か?で判別すれば、ノイズの影響は無視して、元どおりの”01011100″を取り出せる。この0か1かを判別するための区切り(図中青線)は、しきい値と呼ばれる。

また、”01011100″のデータを送る通信の途中で、しきい値を越えるようなノイズが混ざって、受信したとする。この場合、単純に受け取るだけであれば、”01010100″で間違った値を受け取っても判別できない。しかし、データを送る際にパリティビット(偶数パリティであれば全データの1の数が偶数になるように)1ビットのデータを加える。このデータを受け取った際に、ノイズで1ビット反転した場合、1の数が奇数(3個)なので、ノイズでビット反転が発生したことがわかる。これをパリティチェックと言う。

このように、デジタル信号を使えば、しきい値を越えない程度のノイズならノイズを無視できるし、たとえ大きなノイズでデータに間違いがあっても、パリティチェックのような方法を使えば間違って伝わったことを判別できる。

純粋仮想基底クラス

前回説明した仮想関数では、基底クラスから派生させたクラスを作り、そのデータが混在してもクラスに応じた関数(仮想関数)を呼び出すことができる。

この仮想関数の機能を逆手にとったプログラムの記述方法として、純粋仮想基底クラスがある。その使い方を説明する。

純粋仮想基底クラス

純粋仮想基底クラスとは、見かけ上はデータを何も持たないクラスであり、本来なら意味がないデータ構造となってしまう。しかし、派生クラスで仮想関数で機能を与えることで、基底クラスという共通部分から便利な活用ができる。(実際には、型を区別するための情報を持っている)

例えば、一つの配列に、整数、文字列、実数といった異なる型のデータを記憶させることは本来ならできない。しかし、以下のような処理を記載すれば、可能となる。

// 純粋仮想基底クラス
class Object {
public:
   virtual void print() = 0 ;
   // 中身の無い純粋基底クラスで、
   // 仮想関数を記述しない時の書き方。
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%d\n" , data ) ;
   }
} ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() {
      printf( "%s\n" , data ) ;
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%lf\n" , data ) ;
   }
} ;

// 動作確認
int main() {
   Object* data[3] = {
      new IntObject( 123 ) ,
      new StringObject( "abc" ) ,
      new DoubleObject( 1.23 ) ,
   } ;
   for( int i = 0 ; i < 3 ; i++ ) { // 123
      data[i]->print() ;            // abc
   }                                // 1.23 と表示
   return 0 ;
} ;

このプログラムでは、純粋仮想基底クラスObjectから、整数IntObject, 文字列StringObject, 実数DoubleObject を派生させ、そのデータを new により生成し、Objectの配列に保存している。

様々な型に適用できるプログラム

次に、純粋仮想基底クラスの特徴を応用したプログラムの作り方を説明する。

例えば、以下のような最大選択法で配列を並び替えるプログラムがあったとする。

int a[5] = { 11, 55, 22, 44, 33 } ;

void my_sort( int array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j] > array[max] )
            max = j ;
      }
      int tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
int main() {
   my_sort( a , 5 ) ;
}

しかし、この整数を並び替えるプログラムがあっても、文字列の並び替えや、実数の並び替えがしたい場合には、改めて文字列用並び替えの関数を作らなければいけない。しかも、ほとんどが同じような処理で、改めて指定された型のためのプログラムを作るのは面倒である。

C言語のデータの並び替えを行う、qsort() では、関数ポインタを用いることで、様々なデータの並び替えができる。

#include <stdio.h>
#include <stdlib.h>
int a[ 4 ] = { 11, 33, 22, 44 } ;
double b[ 3 ] = { 1.23 , 5.55 , 0.11 } ;
// 並び替えを行いたいデータ専用の比較関数を作る
// a>bなら+1, a=bなら0, a<bなら-1を返す関数
int cmp_int( int* pa , int* pb ) {
   return *pa - *pb ;
}
int cmp_double( double* pa , double* pb ) {
   if ( *pa == *pb )
      return 0 ;
   else if ( *pa > *pb )
      return 1 ;
   else
      return -1 ;
}
int main() {
   qsort( a , 4 , sizeof( int ) ,
          (int(*)(void*,void*)) cmp_int ) ;
   qsort( b , 3 , sizeof( double ) ,
          (int(*)(void*,void*)) cmp_double ) ;
} 

任意のデータを並び替え

class Object {
public:
   virtual void print() = 0 ;
   virtual int cmp( Object* ) = 0 ;
} ;

// 整数データの派生クラス
class IntObject : public Object {
private:
   int data ;
public:
   IntObject( int x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%d\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      int pdata = ((IntObject*)p)->data ;
      return data - pdata ;
   }
} ;

// 文字列の派生クラス
class StringObject : public Object {
private:
   char data[ 100 ] ;
public:
   StringObject( const char* s ) {
      strcpy( data , s ) ;
   }
   virtual void print() {
      printf( "%s\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      char* pdata = ((StringObject*)p)->data ;
      return strcmp( data , pdata ) ; // 文字列比較関数
   }
} ;

// 実数の派生クラス
class DoubleObject : public Object {
private:
   double data ;
public:
   DoubleObject( double x ) {
      data = x ;
   }
   virtual void print() {
      printf( "%lf\n" , data ) ;
   }
   virtual int cmp( Object* p ) {
      double pdata = ((DoubleObject*)p)->data ;
      if ( data == pdata )
         return 0 ;
      else if ( data > pdata )
         return 1 ;
      else
         return -1 ;
   }
} ;

// Objectからの派生クラスでcmp()メソッドを
//   持ってさえいれば、どんな型でもソートができる。
void my_sort( Object* array[] , int size ) {
   for( int i = 0 ; i < size - 1 ; i++ ) {
      int max = i ;
      for( int j = i + 1 ; j < size ; j++ ) {
         if ( array[j]->cmp( array[max] ) > 0 )
            max = j ;
      }
      Object* tmp = array[i] ;
      array[i] = array[max] ;
      array[max] = tmp ;
   }
}
// 動作確認
int main() {
   Object* idata[3] = {
      new IntObject( 11 ) ,
      new IntObject( 33 ) ,
      new IntObject( 22 ) ,
   } ;
   Object* sdata[3] = {
      new StringObject( "abc" ) ,
      new StringObject( "defghi" ) ,
      new StringObject( "c" ) ,
   } ;
   my_sort( idata , 3 ) ; // 整数のソート
   for( int i = 0 ; i < 3 ; i++ )
      idata[i]->print() ;
   my_sort( sdata , 3 ) ; // 文字列のソート
   for( int i = 0 ; i < 3 ; i++ )
      sdata[i]->print() ;
   return 0 ;
} ;

このような方式でプログラムを作っておけば、新しいデータ構造がでてきてもソートのプログラムを作らなくても、比較専用の関数 cmp() を書くだけで良い。

ただし、この並び替えの例では、Object* を IntObject* に強制的に型変換している。
また、このプログラムでは、データを保管するために new でポインタを保管し、データの比較をするために仮想関数の呼び出しを行うことから、メモリの使用効率も処理効率でもあまりよくない。

こういう場合、最近の C++ ではテンプレート機能が使われる。

template <typename T>
void my_sort( T a[] , int size ) {
  for( int i = 0 ; i < size - 1 ; i++ ) {
    int max = i ;
    for( int j = i + 1 ; j < size ; j++ ) { if ( a[j] > a[max] )
        max = j ;
    }
    T  tmp = a[i] ;
    a[i] = a[max] ;
    a[max] = tmp ;
  }
}

int main() {
  int idata[ 5 ] = { 3, 4, 5 , 1 , 2 } ;
  double fdata[ 4 ] = { 1.23 , 0.1 , 3.4 , 5.6 } ;
  my_sort( idata , 5 ) ;
  for( int i = 0 ; i < 5 ; i++ )
    printf( "%d " , idata[i] ) ;
  printf( "\n" ) ;
  my_sort( fdata , 4 ) ;
  for( int i = 0 ; i < 4 ; i++ )
    printf( "%lf " , fdata[i] ) ;
  printf( "\n" ) ;
  return 0 ;
}

C++のテンプレート機能は、my_sort( int[] , int ) で呼び出されると、typename T = int で、整数型用の my_sort() の処理が自動的に作られる。同じく、my_sort( double[] , int ) で呼び出されると、typename = double で 実数型用の my_sort() が作られる。

mallocを使った課題

授業での malloc , free を使ったプログラミングを踏まえ、以下のレポートを作成せよ。

以下のデータのどれか1つについて、データを入力し、何らかの処理を行うこと。
課題は、原則として、(自分の出席番号%3)+1 についてチャレンジすること。

  1. 名前と電話番号
  2. 名前と年齢(もしくは生年月日)
  3. 名前と身長・体重

このプログラムを作成するにあたり、以下のことを考慮しmallocを適切に使うこと。

  • 名前は、長い名前の人が混ざっているかもしれない。
  • 保存するデータ件数は、10件かもしれない1000件かもしれない。(データ件数は、処理の最初に入力すること。)

ただし、mallocの理解に自信がない場合は、名前もしくはデータ件数のどちらか一方は固定値でも良い。

レポートには、(a)プログラムリスト, (b)プログラムの説明, (c)正しく動いたことがわかる実行例, (d)考察 を記載すること。

考察には、自分のプログラムが正しく動かない事例はどういう状況でなぜ動かないのか…などを検討したり、プログラムで良くなった点はどういう所かを説明すること。

malloc()とfree()

malloc()とfree()

malloc() は、動的(ヒープ領域)にメモリを確保する命令で、データを保存したい時に malloc() を実行し、不要になった時に free() を実行する。

malloc() では、alloca() と同じように、格納したいデータの byte 数を指定する。また、malloc() は、確保したメモリ領域の先頭を返すが、ヒープメモリが残っていない場合 NULL ポインタを返す。処理が終わってデータ領域をもう使わなくなったら、free() で解放する必要がある。

基本的には、確保したメモリ領域を使い終わった後 free() を実行しないと、再利用できないメモリ領域が残ってしまう。こういう処理を繰り返すと、次第にメモリを食いつぶし、仮想メモリ機能によりハードディスクの読み書きで性能が低下したり、最終的にOSが正しく動けなくなる可能性もある。こういった free() 忘れはメモリーリークと呼ばれ、malloc(),free()に慣れない初心者プログラマーによく見られる。

ヒープメモリは、プロセスの起動と共に確保され、プログラムの終了と同時にOSに返却される。このため、malloc()と処理のあとすぐにプロセスが終了するようなプログラムであれば、free() を忘れても問題はない。授業では、メモリーリークによる重大な問題を理解してもらうため、原則 free() は明記する。

文字列を保存する場合

#include <stdlib.h>
char* names[ 10 ] ;
char  buff[ 1000 ] ;

// 名前を10件読み込む
void inputs() {
   for( int i = 0 ; i < 10 ; i++ ) {
      if ( fgets( buff ,
                  sizeof( buff ) ,
                  stdin ) != NULL ) {
         names[ i ]
            = (char*)malloc( strlen(buff)+1 ) ;
         if ( names[ i ] != NULL )
            strcpy( names[ i ] , buff ) ;
      }
   }
}
// 名前を出力する
void prints() {
   for( int i = 0 ; i < 10 ; i++ )
      printf( "%s" , names[ i ] ) ;
}
void main() {
   // 文字列の入力&出力
   inputs() ;
   prints() ;
   // 使い終わったら、free() で解放
   for( int i = 0 ; i < 10 ; i++ )
      free( names[ i ] ) ;
}

文字列を保存する場合には、上記の names[i] への代入のような malloc() と strcpy() を使うことが多い。
このための専用の関数として、strdup() がある。基本的には、以下のような機能である。

char* strdup( char* s ) {
   char* p ;
   if ( (p = (char*)malloc( strlen(s)+1 )) != NULL )
      strcpy( p , s ) ;
   return p ;
}

また、入力した文字列をポインタで保存する場合、以下のようなプログラムを書いてしまいがちであるが、図に示すような状態になることから、別領域にコピーする必要がある。

char  buff[ 1000 ] ;
char* name[10] ;
for( int i = 0 ; i < 10 ; i++ ) {
   if ( fgets( buff , sizeof(buff) , stdin ) != NULL )
      name = buff ;
}

配列に保存する場合

任意サイズの配列を作りたい場合には、malloc() で一括してデータの領域を作成し、その先頭アドレスを用いて配列として扱う。

#include <stdlib.h>
void main() {
   int size ;
   int* array ;
   // 処理するデータ件数を入力
   scanf( "%d" , &size ) ;

   // 整数配列を作る
   if ( (array = (int*)malloc( sizeof(int) * size )) != NULL ) {
      int i ;
      for( i = 0 ; i < size ; i++ )
         array[i] = i*i ; // あんまり意味がないけど
      for( i = 0 ; i < size ; i++ )
         printf( "%d¥n" , array[i] ) ;

      // mallocしたら必ずfree
      free( array ) ;
   }
}

構造体の配列

同じように、任意サイズの構造体の配列を作りたいのであれば、配列サイズに「sizeof( struct Complex ) * データ件数」を指定すればいい。

#include <stdlib.h>
struct Complex {
   double re , im ;
} ;

// 指定した場所にComplexを読み込む。
int input_Complex( struct Complex* p ) {
   return scanf( "%f %f" ,
                 &(p->re) , &(p->re) ) == 2 ;
}

// 指定したComplexを出力
void print_Complex( struct Complex* p ) {
   printf( "%f+j%f¥n" , p->re , p->im ) ;
}
void main() {
   int size ;
   struct Complex* array ;
   // 処理する件数を入力
   scanf( "%d" , &size ) ;
   // 配列を確保して、データの入力&出力
   if ( (array = (struct Complex*)malloc(
                    sizeof(struct Complex) * size )) != NULL ) {
      int i ;
      for( i = 0 ; i < size ; i++ )
         if ( !input_Complex( &array[i] ) )
            break ;
      for( i = 0 ; i < size ; i++ )
         print_Complex( &array[i] ) ;

      // mallocしたら必ずfree
      free( array ) ;
   }
}

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー