ホーム » 「C言語」タグがついた投稿
タグアーカイブ: C言語
ガベージコレクタ
ガベージコレクタ
では、循環リストの発生するようなデータで、共有が発生するような場合には、どのようにデータを管理すれば良いだろうか?
最も簡単な方法は、処理が終わっても、使い終わったメモリを返却しない、方法である。ただし、このままでは、メモリを使い切ってしまう。
そこで、廃棄処理をしないまま、ゴミだらけになってしまったメモリ空間を再利用するのが、ガベージコレクタ(一般的にはGCと略される)である。
ガベージコレクタは、貸し出すメモリ空間が無くなった時に起動され、
- すべてのメモリ空間に、「不要」の目印をつける。(unmark処理)
- 変数に代入されているデータが参照している先のデータは「使用中」の目印をつける。(mark処理-目印をつける)
- その後、「不要」の目印がついている領域は、だれも使っていないので回収する。(sweep処理-掃き掃除する)
この方式は、マークアンドスイープ法と呼ばれる。ただし、このようなガベージコレクタはメモリ空間が広い場合は、処理時間かかり、さらにこの処理中は、他の処理ができず処理が中断されるので、コンピュータの操作性という点では問題となる。
最近のプログラミング言語では、参照カウンタとガベージコレクタを取り混ぜた方式でメモリ管理をする機能が組み込まれている。このようなシステムでは、局所変数のような関数に入った時点で生成され関数終了ですぐに不要となる領域は、参照カウンタで管理し、大域変数のような長期間保管するデータはガベージコレクタで管理される。
大量のメモリ空間で、メモリが枯渇したタイミングでガベージコレクタを実行すると、長い待ち時間となることから、ユーザインタフェースの待ち時間に、ガベージコレクタを少しづつ動かすなどの方式もとることもある。
ガベージコレクタが利用できる場合、メモリ管理を気にする必要はなくなってくる。しかし、初心者が何も気にせずプログラムを書くと、使われないままのメモリがガベージコレクタの起動まで放置され、場合によってはメモリ不足による処理速度低下の原因となる場合もある。手慣れたプログラマーであれば、素早くメモリを返却するために、使われなくなった変数に意図的に null を代入するなどのテクニックを使う。
プログラム言語とメモリ管理機能
一般的に、C言語というとポインタの概念を理解できないと使えなかったり、メモリ管理をきちんとできなければ危険な言語という点で初心者向きではないと言われている。
C言語は、元々 BCPL や B言語を改良してできたプログラム言語であった。これに、オブジェクト指向の機能を加えた C++ が作られた。C++ という言語の名前は、B言語→C言語と発展したので、D言語(現在はまさにD言語は存在するけど)と名付けようという意見もあったが、C++ を開発したビャーネ・ストロヴストルップは、ガベージコレクタのようなメモリ管理機能が無いことから、D言語を名乗るには不十分ということで、C言語を発展させたものという意味でC++と名付けている。
こういった中で、C++をベースとしたガベージコレクタなどを実装した言語としては、Java が挙げられる。オブジェクト指向をベースとしたマルチスレッドやガベージコレクタに加え、仮想マシンによる実行で様々なOS(やブラウザ)で動かすことができる。
最近注目されている言語の1つとして、C言語の苦手であった「メモリ安全性」や実行効率を考えて開発されたものに Rust が挙げられる。メモリ管理や効率などの性能から、最近では Linux の開発言語に Rust を部分的に導入するなどの計画も出ている。
C言語での入出力処理のおさらい
テストのプログラム作成の問題で、入力処理の書き方が適切でないものが多いので、基本とテクニックの解説。
scanf()の使い方
// scanf( "フォーマット" , 引数... ) ; // データの型とフォーマット // int %d (10進数として入力) - Digit // %o (8進数として入力) - Octal digit // %x (16進数として入力) - heXa-decimal digit // - short int %hd - half-size digit // - long int %ld - long-size digit // // float %f // double %lf - long-size float // // char[] %s // char %c // 基本 scanf() はポインタ渡し。文字列(char配列)は配列の先頭アドレスを渡す int x ; scanf( "%d" , &x ) ; // ポインタを渡し、xの場所に書き込んでもらう。 char str[ 10 ] ; scanf( "%s" , str ) ; // strは配列の先頭の場所。 & は不要。
通常のscanfの%d,%sなどは、データ区切りは空白や改行となる。
%s で “tohru saitoh”といった空白の入った文字列を入力するには、一工夫が必要。
printf()の使い方
// printf( "フォーマット" , 引数... ) ; // データの型とフォーマット // 基本は、scanf() と同じ。 // float 型の出力 // %f 固定小数点表示 1.23 のような出力 // %e 指数表示 1.23e+10 のような出力 // %g 固定小数点%fと指数表示%eのどちらか // 桁数指定 // %5d - 必ず5桁で出力 " 123" // %05d - 5桁の空白部は0で埋める "00123" // %5.2f - 全体5桁、小数点以下2桁 " 1.23" // %5s - 文字列を5桁で出力 " abc" // 左寄せ // %-5s - 文字列を左寄せ5桁で出力"abc "
scanf(),printf()は成功した項目数を返す
scanf(),printf() は、返り値を使わないように思うけど、実際は入力に成功した項目件数、出力に成功した項目件数を返す。
int x ; char str[ 10 ] ; int ans ; ans = scanf( "%d%s" , &x , str ) ; printf( "%d¥n" , ans ) ; // 入力に成功すれば2。 ans = printf( "%d%s¥n" , x , str ) ; printf( "%d¥n" , ans ) ; // 出力に成功すれば2。
特に、ファイルからの入力であれば、途中でデータがなくなって、これ以上データが入力できない時には、scanfの返り値を用いる。
char name[ 100 ] ; int age ; while( scanf( "%s%d" , name , &age ) == 2 ) { printf( "%s %d¥n" , name , age ) ; }
Microsoft の scanf_s() は安全な入力関数
C言語の標準関数 scanf() の %s では、バッファオーバーフローを検出できない。Microsoft の Visual Studio の C言語では、こういった危険な scanf(“%s”) は危険なので、scanf_s() を使うようになっている。ただし、他のC言語では使えない場合が多いので要注意。
char name[ 10 ] ; scanf( "%s" , name ) ; // バッファオーバーフロー scanf_s( "%s" , name , sizeof( name ) ) ; // バッファオーバーフローの心配がない
fget()とsscanf()を使った安全な入力
C言語では、1行のデータを読む場合には、gets() 関数がある。しかし、配列サイズを指定できないので、バッファオーバーフローの危険がある。一方、ファイルからの入力関数 fgets() は、入力するデータサイズを指定できるため、fgets を使うべき。
char buff[ 1000 ] ; gets( buff ) ; // バッファオーバーフローの危険性 fgets( buff , sizeof( buff ) , stdin ) ; // 入力に失敗すると NULLを返す
一方、入力した文字列のデータから、scanf() のようにデータを抽出する、sscanf() という関数がある。
char string[] = "123 tohru 1.23" ; int x ; char str[ 10 ] ; double y ; sscanf( string , "%d%s%lf" , &x , str , &y ) ; // x = 123 , str = "tohru" , y = 1.23 // scanfと同様に成功した項目数3が返る。
これらを踏まえ、1行に名前と年齢のデータが記録されていて、データが入力できるだけ繰り返す処理を fgets + sscanf で書くと以下のようになる。
char buff[ 1000 ] ; char name[ 1000 ] ; int age ; while( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { if ( sscanf( buff , "%s%d" , name , &age ) == 2 ) { // buffには必ず1000文字以下なので、nameが1000文字を超えることはない printf( "%s %d¥n" , name , age ) ; } }
なお、このプログラムを動かす場合、これ以上データがない場合には、Windowsであれば “Ctrl-Z” を入力。unixやmacOSであれば”Ctrl-D”を入力すること。
その他の気づいた点
文末の「;」を忘れないで
JavaScript では、単純式の末尾の「;」は、忘れてもそれなりに正しく動く。しかし、C言語では「;」を忘れると、文法エラーになるので要注意。
1: struct A { 2: : 3: } ; ←この行のセミコロンを忘れると、5行目で文法エラーの表示となる。 4: 5: for( ... ) { ←この行を見ても文法エラーの原因はわからない。3行目が原因。 6: : 7: }
局所変数やヒープメモリにはゴミデータが入っている
void foo() { int sum ; ←初期化忘れ int array[ 3 ] = { 11 , 22 , 33 } ; for( int i = 0 ; i < 3 ; i++ ) { sum += array[ i ] ; } }
局所変数やヒープメモリは、関数に入って確保やmallocで確保されるメモリ領域だけど、このメモリ領域には以前の処理で使われていたデータが残っている場合がある。こういった初期化されていないメモリ領域は、悪意を持ったプログラムがデータを盗むために使われる場合もある。必要に応じてちゃんと初期化が必要。
また、大域変数(グローバル変数)は、C言語では 0 で初期化される。(ポインタなら NULL が入っている。)
プログラム言語(C言語)の基礎
学際科目の情報制御基礎において、学科間でプログラミングの初歩の理解差があるので、簡単なC言語プログラミングの基礎の説明。
Hello World
“Hello World”と表示するだけのC言語プログラムは以下のようになる。
// コメントの書き方1 // "//"で始まる行は、プログラムの説明(コメント) /* コメントの書き方2 */ // "/*"から"*/"で囲まれる範囲もコメント #include <stdio.h> // #で始まる行はプリプロセッサ行 // stdio.h には、入出力関数の説明が書いてある int main() { // 一連の処理の塊を関数と呼ぶ。 // C言語では main() 関数を最初に実行する。 printf( "Hello World\n" ) ; // printf() は、以下の物を表示する関数。 // "\n"は、文字を出力して改行するための特殊文字 return 0 ; // main() 関数が、正常終了したことを意味する } // 0 を返り値として返す。
“#include <…>”のプリプロセッサ行は、最初のうちは解りにくいので、「これを書かないとダメ…」と思っていればいい。
#include <stdio.h> は、別ファイル(ヘッダファイル) stdio.h に記載されているプログラムリストを読み込む機能。
stdio.h には、printf() や scanf() などの基本的な関数や定数などの情報が記載されている。
C言語の基本的な命令(文)は、”;”で終わる。(単文)
複数の処理をまとめる場合には、”{“から”}”の中に、複数の文を書き並べる。(複文)
関数とは、複数の処理をひとまとめにした、処理の「かたまり」と思えばいい。
関数の型 関数名( 仮引数 ... ) { 処理1 ... ; 処理2 ... ; }printf() の 文字列中の”\n”(あるいは”¥n”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。
変数と代入
#include <stdio.h> #include <math.h> // 数学関数を使う 平方根 sqrt() を使っている int main() { // 変数の宣言 int i ; // 符号付き32bit変数 i の宣言 int a = 123 , j ; // a を 123 で初期化 , j も整数型 float x ; // 単精度実数の x を宣言 double y = 1.234 , z ; // 倍精度実数の y を宣言し 1.234 で初期化, // z も倍精度実数 // 変数への代入 i = 1 ; // i に 1 を代入 i = 12 + 2 * a ; // 12+2*a を代入 a は123なので、 // iには、258 が入る。 x = sqrt( 2.0 ) ; // x に 2.0 の平方根(1.4142)を代入 z = y * 2.0 + x * 3.0 ; // y*2+x*3をzに代入 // 変数の内容の表示 printf( "%d\n" , i ) ; // 整数型(%d)で、 i の値を表示 printf( "%f\n" , x ) ; // 単精度実数(%f) で、x の値を表示 printf( "%lf\n" , z ) ; // 倍精度実数(%lf)で、z の値を表示 printf( "iの値は%d,xの値は%lfです。\n" , i , x ) ; return 0 ; // 正常終了 0 を返す }
変数(計算結果を格納する入れ物)を使う場合は、変数を宣言する。
変数名には、何が入っているのか理解しやすいように、名前をつければいい。(英字で始まり、英数字が続くもの,_が入ってもいい)
変数に値を記憶する時は、”変数名=式 ;”の様に書くと、代入演算子”=” の右辺を計算し、その計算結果が左辺の変数に保存される。
変数の内容を表示する時には、printf() の文字列の中に、%d,%f,%lf などの表示したい式の型に応じたものを書いておく。
式の値が、その %.. の部分に書き込まれて、出力される。
繰り返しの制御命令
最も基礎的な繰り返し命令として、for() 文を説明。
#include <stdio.h> int main() { int i ; for( i = 1 ; i <= 10 ; i++ ) { // iを1から10まで変化させる。 printf( "%d %d\n" , i , i*i ) ; // i と iの二乗を表示 } return 0 ; }
for文の意味を説明するために、対応するフローチャートを示す。
先のプログラムをフローチャートで示し、その命令の実行順序と、その変数の変化を下図に示す。
練習問題1
簡単なプログラミングの練習として、前回講義の練習問題をC言語で書いてみよう。
- 電気電子工学科,電子情報工学科の学生は、出席番号が奇数は処理C,偶数は処理Dについて回答せよ。
- それ以外の学科の学生は、出席番号が奇数は処理A,偶数は処理Bの結果について回答せよ。
制御構文とフローチャート
構文の入れ子
文と複文
C言語の文法で、{,} は複数の処理をまとめる複文とよばれる。
これに対して、a = 123 ; といったセミコロンで終わる「処理 ;」は単文という。
制御構文は、「if ( 条件) 文」で文となる。このため、文が単文であれば、{,} は不要である。
if ( 条件 ) { a = 123 ; } if ( 条件 ) a = 123 ; // 単文なら中括弧は不要
同じように、「while(条件) 文」、「for(A,B,C) 文」、「do 文 while(条件) ;」も、それぞれ文を構成する。
{,} の複文は、{ 文 文 文… } のように、一連の文を実行し、それを1つの文として扱うための機能である。
練習問題2
プログラムの制御構造の確認として、以下の3つ(No.1,No.2,No.3)の問題から、
M科,C科,B科の学生は((自分の出席番号+1) % 2)+1 の問題、E科,EI科の学生は、((自分の出席番号+1) % 3)+1について、プログラムのフローチャートを描き、その処理がどのように進むのか答えよ。
レポートには、以下の点を記載すること。
- フローチャート
- 実行順序
- 変数の変化がわかる内容
- (できれば、実際にプログラムを動かし、正しいことを検証)
// No.1 #include <stdio.h> int main() { int i , j ; for( i = 1 ; i <= 4 ; i++ ) { if ( i % 2 == 0 ) { // i%2 は2で割った余り,i%2==0ならば偶数のとき for( j = 1 ; j <= 2 ; j++ ) printf( "%d %d\n" , i , j ) ; } } return 0 ; } // No.2 #include <stdio.h> int main() { int x = 10 , y = 7 , s = 0 ; while( x > 0 ) { if ( x % 2 != 0 ) s = s + y ; y = y * 2 ; x = x / 2 ; // 注意: xは整数型 } printf( "%d\n" , s ) ; return 0 ; } // No.3 #include <stdio.h> int a[ 6 ] = { 2 , 3 , 5 , 8 , 13 , 21 } ; int main() { int left = 0 , right = 6 , mid ; int key = 13 ; while( right - left > 0 ) { mid = (left + right) / 2 ; // 整数型で計算 printf( "%d\n" , a[ mid ] ) ; if ( a[ mid ] == key ) break ; else if ( a[ mid ] > key ) right = mid ; else left = mid + 1 ; } return 0 ; }
C言語の基礎(part1)
学際科目の情報制御基礎において、学科間でプログラミングの初歩の理解差があるので、簡単なC言語プログラミングの基礎の説明。
Hello World
“Hello World”と表示するだけのC言語プログラムは以下のようになる。
// コメントの書き方1 // "//"で始まる行は、プログラムの説明(コメント) /* コメントの書き方2 */ // "/*"から"*/"で囲まれる範囲もコメント #include <stdio.h> // #で始まる行はプリプロセッサ行 // stdio.h には、入出力関数の説明が書いてある int main() { // 一連の処理の塊を関数と呼ぶ。 // C言語では main() 関数を最初に実行する。 printf( "Hello World\n" ) ; // printf() は、以下の物を表示する関数。 // "\n"は、文字を出力して改行するための特殊文字 return 0 ; // main() 関数が、正常終了したことを意味する } // 0 を返り値として返す。
“#include <>”のプリプロセッサ行は、最初のうちは解りにくいので、これを書かないとダメ…と思っていればいい。
#include <stdio.h> は、別ファイル(ヘッダファイル) stdio.h に記載されているプログラムを読み込む機能。
stdio.h には、printf() とか scanf() とかの関数や定数などの情報が記載されている。
C言語の基本的な命令(文)は、”;”で終わる。(単文)
複数の処理をまとめる場合には、”{“から”}”の中に、複数の文を書き並べる。(複文)
関数とは、複数の処理をひとまとめにした、処理の「かたまり」と思えばいい。
関数の型 関数名( 仮引数 ... ) { 処理1 ... ; 処理2 ... ; }printf() の 文字列中の”\n”(あるいは”¥n”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。
変数と代入
#include <stdio.h> #include <math.h> // 数学関数を使う 平方根 sqrt() を使っている int main() { // 変数の宣言 int i ; // 符号付き32bit変数 i の宣言 int a = 123 , j ; // a を 123 で初期化 , j も整数型 float x ; // 単精度実数の x を宣言 double y = 1.234 , z ; // 倍精度実数の y を宣言し 1.234 で初期化, // z も倍精度実数 // 変数への代入 i = 1 ; // i に 1 を代入 i = 12 + 2 * a ; // 12+2*a を代入 a は123なので、 // iには、258 が入る。 x = sqrt( 2.0 ) ; // x に 2.0 の平方根(1.4142)を代入 z = y * 2.0 + x * 3.0 ; // y*2+x*3をzに代入 // 変数の内容の表示 printf( "%d\n" , i ) ; // 整数型(%d)で、 i の値を表示 printf( "%f\n" , x ) ; // 単精度実数(%f) で、x の値を表示 printf( "%lf\n" , z ) ; // 倍精度実数(%lf)で、z の値を表示 printf( "iの値は%d,xの値は%lfです。\n" , i , x ) ; return 0 ; // 正常終了 0 を返す }
変数(計算結果を格納する入れ物)を使う場合は、変数を宣言する。
変数名には、何が入っているのか理解しやすいように、名前をつければいい。(英字で始まり、英数字が続くもの,_が入ってもいい)
変数に値を記憶する時は、”変数名=式 ;”の様に書くと、代入演算子”=” の右辺を計算し、その計算結果が左辺の変数に保存される。
変数の内容を表示する時には、printf() の文字列の中に、%d,%f,%lf などの表示したい式の型に応じたものを書いておく。
式の値が、その %.. の部分に書き込まれて、出力される。
繰り返しの制御命令
最も基礎的な繰り返し命令として、for() 文を説明。
#include <stdio.h> int main() { int i ; for( i = 1 ; i <= 10 ; i++ ) { // iを1から10まで変化させる。 printf( "%d %d\n" , i , i*i ) ; // i と iの二乗を表示 } return 0 ; }
for文の意味を説明するために、対応するフローチャートを示す。
先のプログラムをフローチャートで示し、その命令の実行順序と、その変数の変化を下図に示す。
理解度確認
以下のプログラムの実行順序と、最終結果で表示される内容を答えよ。
局所変数配列をreturn
情報構造論の前期期末試験で、局所変数返しの解答が多かったので、メモ。
JavaScript でプログラムを書いていると、動くネタだけど、C言語では初心者がよく間違って書いてしまう定番であり、それなりに動いたりするから、誤解が増えてしまう可能性もある。
スタック変数返し
char* foo() { char str[ 20 ] ; strcpy( str , "Hello World" ) ; return str ; } char* bar() { char baz[ 20 ] ; memset( baz , 'A' , sizeof( baz ) ) ; return NULL ; } void main() { char* ans = foo() ; printf( "%s¥n" , ans ) ; // Hello World // 一見正しく動いているように思うかも。 bar() ; // 局所変数を触るだけの処理 printf( "%s¥n" , ans ) ; // AAAAAAAAAAA // ans の中身が壊れている。 }
静的局所変数返し
上記のスタック変数を返す問題点を示すと、じゃあ静的局所変数でいいじゃん….という話がでてくる。
char* foo() { static char str[20] ; strcpy( str , "Hello World" ) ; return str ; }
この方式なら、スタック領域ではなく静的変数領域なので、関数呼び出しで破壊されることはない。
しかし、この方式は「スレッドセーフ」ではない。foo() の処理後に、スレッドを起動した場合、スレッドではメモリ空間が共有されるため、foo() を保持していて、別スレッドがそのメモリを書き換えると、他方は中身が変わってしまう。
(参考「スレッドセーフではないCライブラリ関数」)
実数と整数の変換
情報制御基礎で出題した問題で、5点の移動平均の処理の一部に、以下のコードがあった場合の間違い説明の問題。
int avg() { int s = .... ; return (1/5) * s ; }
の間違いを修正せよ….の答えだけど、return 0.2 * s ; とか return s/5 ; が答えだけど、小数点以下が切り捨てになるのか、四捨五入になるのか気になってきた。
for( double x = 0.0 ; x <= 2.0 ; x += 0.1 ) printf( "%ld %d\n" , x , (int)x ) ;
で確認してみたら、(int)実数は、小数点以下切り捨てだな。となると、四捨五入したいなら、return (int)( 0.2 * s + 0.5 ) ; とか、return (s+2)/5 ; とか書いてほしい…とか微妙な話になるな。
採点は、切り捨て・四捨五入の誤差については問わないで、0.2*s , s/5 を◯でいこう。