プログラム言語(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”)は、改行を意味する。
「\:バックスラッシュ」は、日本語環境では「¥:円記号」で入力・表示することが多い。
Paiza.io で動かしてみよう
C言語を本格的に使いたいなら、Microsoft Visual Studio などをインストールして使う方が便利だが、情報制御基礎で説明する程度のプログラムなら、Paiza.io が便利。ブラウザの画面で簡単にプログラムの動作を確認することができる。https://paiza.io/jaにアクセスして、上述の Hello World を動かしてみよう。
変数と代入
#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 などの表示したい式の型に応じたものを書いておく。%d=int型 , %f=float型 , %lf=double型
式の値が、その %.. の部分に書き込まれて、出力される。
繰り返しの制御命令
最も基礎的な繰り返し命令として、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の結果について回答せよ。
- 自分が考えたプログラムは、前述の Paiza.io や、自分のパソコンのC言語環境で入力し、動作結果も確認せよ。
制御構文とフローチャート
構文の入れ子
文と複文
C言語の文法で、{,} は複数の処理を連続して実行し、複文とよばれる。複数ので文を構成する。
これに対して、a = 123 ; といったセミコロンで終わる「処理 ;」は単文といい、1つの式で文となる。
制御構文のif文は、「if ( 条件 ) 文真」で文となる。このため条件が満たされたときに実行する文真が単文であれば、{,} は不要である。条件が満たされない場合の処理も記述するときには、「if ( 条件 ) 文真 else 文偽」を使う。
// if文 if ( 条件 ) { a = 123 ; } if ( 条件 ) a = 123 ; // 単文なら中括弧は不要 // if-then-else if ( x >= 60 ) { printf( "合格点\n" ) ; } else { printf( "不合格点\n" ) ; }
同じように、「while(条件) 文」、「for(A,B,C) 文」、「do 文 while(条件) ;」も、それぞれ文を構成する。
{,} の複文は、{ 文 文 文… } のように、一連の文を実行し、それを1つの文として扱うための機能である。
// while 文 i = 0 ; while( i < 10 ) { printf( "%d\n" , i ) ; i++ ; } // for 文 for( i = 0 ; i < 10 ; i++ ) { printf( "%d\n" , i ) ; } // do-while 文 i = 0 ; do { printf( "%d\n" , i ) ; i++ ; } while( i < 10 ) ;
練習問題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 ; }
派生と継承と仮想関数
前回の派生と継承のイメージを改めて記載する。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; saitoh.print() ; // 表示 t-saitoh 55 yama.print() ; // 表示 yamada 21 // - ES 1 nomu.print() ; // 表示 nomura 22 return 0 ; // - PS 2 }
このような処理でのデータ構造は、次のようなイメージで表される。
派生クラスでの問題提起
基底クラスのオブジェクトと、派生クラスのオブジェクトを混在してプログラムを記述したらどうなるであろうか?
上記の例では、Person オブジェクトと、Student オブジェクトがあったが、それをひとまとめで扱いたいこともある。
以下の処理では、Person型の saitoh と、Student 型の yama, nomu を、一つの table[] にまとめている。
int main() { Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[ i ]->print() ; } return 0 ; }
C++では、Personへのポインタの配列に代入する時、Student型ポインタは、その基底クラスへのポインタとしても扱える。ただし、このように記述すると、table[] には、Person クラスのデータして扱われる。
このため、このプログラムを動かすと、以下のように、名前と年齢だけが3人分表示される。
t-saitoh 55 yamada 21 nomura 22
派生した型に応じた処理
上記のプログラムでは、 Person* table[] に、Person*型,Student*型を混在して保存をした。しかし、Person*として呼び出されると、yama のデータを表示しても、所属・学年は表示されない。上記のプログラムで、所属と名前を表示することはできないのだろうか?
// 混在したPersonを表示 for( int i = 0 ; i < 3 ; i++ ) table[i]->print() ; // Student は、所属と名前を表示して欲しい t-saitoh 55 yamada 21 - ES 1 nomura 22 - PS 2
上記のプログラムでは、Person型では、後でStudent型と区別ができないと困るので、Person型に、Person型(=0)なのか、Student型(=1)なのか区別するための type という型の識別番号を追加し、type=1ならば、Student型として扱うようにしてみた。
// 基底クラス class Person { private: int type ; // 型識別情報 char name[ 20 ] ; int age ; public: Person( int tp , const char s[] , int x ) : type( tp ) , age( x ) { strcpy( name , s ) ; } int type_person() { return type ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( int tp , const char s[] , int x , const char d[] , int g ) : Person( tp , s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( 0 , "t-saitoh" , 55 ) ; Student yama( 1 , "yamada" , 21 , "ES" , 1 ) ; Student nomu( 1 , "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { switch( table[i]->type_person() ) { case 0 : table[i]->print() ; break ; case 1 : // 強制的にStudent*型として print() を呼び出す。 // 最近のC++なら、(static_cast<Student*>(table[i]))->>print() ; ((Student*)table[i])->print() ; break ; } } return 0 ; }
しかし、このプログラムでは、プログラマーがこのデータは、Personなので type=0 で初期化とか、Studentなので type=1 で初期化といったことを記述する必要がある。
また、関数を呼び出す際に、型情報(type)に応じて、その型にふさわしい処理を呼び出すための switch 文が必要になる。
もし、派生したクラスの種類がいくつもあるのなら、(1)型情報の代入は注意深く書かないとバグの元になるし、(2)型に応じた分岐処理は巨大なものになるだろう。実際、オブジェクト指向プログラミングが普及する前の初期の GUI プログラミングでは、巨大な switch 文が問題となっていた。巨大な switch 文は、選択肢だけの if else-if else-if が並ぶと処理効率も悪い。
仮想関数
上記の、型情報の埋め込みと巨大なswitch文の問題の解決策として、C++では仮想関数(Virtual Function)が使える。
型に応じて異なる処理をしたい関数があったら、その関数の前に virtual と書くだけで良い。このような関数を、仮想関数と呼ぶ。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } virtual void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } virtual void print() { Person::print() ; // 基底クラスPersonで名前と年齢を表示 printf( "- %s %d\n" , dep , grade ) ; } } ; int main() { // type=0 は Person 型、type=1は Student 型 Person saitoh( "t-saitoh" , 55 ) ; Student yama( "yamada" , 21 , "ES" , 1 ) ; Student nomu( "nomura" , 22 , "PS" , 2 ) ; Person* table[3] = { &saitoh , &yama , &nomu , } ; for( int i = 0 ; i < 3 ; i++ ) { table[i]->print() ; } return 0 ; }
クラスの中に仮想関数が使われると、C++ では、プログラム上で見えないが、何らかの型情報をオブジェクトの中に保存してくれる。
また、仮想関数が呼び出されると、その型情報を元に、ふさわしい関数を自動的に呼び出してくれる。このため、プログラムも table[i]->print() といった極めて簡単に記述できるようになる。
関数ポインタ
仮想関数の仕組みを実現するためには、関数ポインタが使われる。
以下の例では、返り値=int,引数(int,int)の関数( int(*)(int,int) )へのポインタfpに、最初はaddが代入され、(*fp)(3,4) により、7が求まる。
int add( int a , int b ) { return a + b ; } int mul( int a , int b ) { return a * b ; } int main() { int (*fp)( int , int ) ; fp = add ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3+4=7 fp = mul ; printf( "%d\n" , (*fp)( 3 , 4 ) ) ; // 3*4=12 int (*ftable[2])( int , int ) = { add , mul , } ; for( int i = 0 ; i < 2 ; i++ ) printf( "%d\n" , (*ftable[i])( 3 , 4 ) ) ; return 0 ; }仮想関数を使うクラスが宣言されると、一般的にそのコンストラクタでは、各クラス毎の仮想関数へのポインタのテーブルが型情報として保存されるのが一般的。仮想関数の呼び出しでは、仮想関数へのポインタを使って処理を呼び出す。このため効率よく仮想関数を動かすことができる。
仮想関数の実装方法
仮想関数の一般的な実装方法としては、仮想関数を持つオブジェクトには型情報として仮想関数へのポインタテーブルへのポインタを保存する。この場合、仮想関数の呼び出しは、object->table[n]( arg… ) のような処理が行われる。
Webプログラミングとセキュリティ
ここまでの授業では、Webを使った情報公開で使われる、HTML , JavaScirpt , PHP , SQL などの解説を行ってきたが、これらを組み合わせたシステムを構築する場合には、セキュリティについても配慮が必要である。
今回は、初心者向けの情報セキュリティの講習で使われるCTFという競技の練習問題をつかって、ここまで説明してきた Web の仕組みを使ったセキュリティの問題について解説を行う。
バックエンドと所有権の設定
前回の講義でファイルのパーミッション(読み書き権限)について確認したが、バックエンドプログラミングで必要となるファイルの所有権の設定を通して、演習を行う。これに合わせ、サーバ上のファイルの編集作業なども体験する。
サーバ上のファイルの編集
以前のバックエンドのプログラムの演習ではサーバの設定などの体験もできていないため、フロントエンドの処理でサーバ上に送られたデータは、最終的な書き込み処理は行っていなかった。今回は、サーバ上でデータをサーバ上のバックエンドプログラムの PHP ファイルを修正し、データが書き込めるようにプログラムの修正を行う。
サーバ上のファイルを編集するには、色々な方法がある。
- サーバ上のエディタで直接編集
- unix のシステムで直接ファイルを編集するのであれば、vim や emacs を利用するのが一般的であろう。これらのエディタはリモートサーバにsshなどでログインしている時は、端末ソフトの文字表示機能だけで動作し、GUI 機能を使わない。vim や emacs は、古くから使われ、Windows で動く vim や emacs もある。
- システム管理者権限で編集する必要があるファイルの場合は、以下に紹介するような方法は煩雑であり、サーバ上で直接編集も知っておくべき。
- プログラムをローカルPCで編集しアップロード(今回はコレ)
- 前回の演習では、リモートサーバに接続する際には ssh コマンドを用いたが、ssh にはファイル転送のための scp コマンドも用意されている。
- scp コマンドは、通常の cp 命令 ( cp コピー元 コピー先 ) を ssh のプロトコルでリモートする機能を拡張したものであり、リモートのコンピュータをコピー元やコピー先として指定する場合は、 ユーザ名@リモートホスト:ファイル場所 と記載する。
-
# remotehostのファイル helloworld.c をローカルホストのカレントディレクトリ.にダウンロード C:\Users\tsaitoh> scp tsaitoh@remotehost:helloworld.c . # ローカルホストの foobar.php を remotehostの/home/tsaitoh/public_html/ フォルダにアップロード C:\Users\tsaitoh> scp foobar.php tsaitoh@remotehost:/home/tsaitoh/public_html/
- VSCode でリモートファイルを編集
- 最近のエディタでは、前述のローカルPCで編集しアップロードといった作業を、自動的に行う機能が利用できる。emacs の tramp-mode や、VS Code の Remote ssh プラグインなどがこれにあたる。利用する演習用のサーバが高機能であれば、vscode + remote-ssh が一番便利と思われるが、remote-ssh はサーバで大きな node.js を動かすため、サーバ負担が大きいので今回はこの方式は使わない。
Webアプリと所有権の問題
PHPで書かれたバックエンドでのプログラムにおいて、Webサーバは www-data(uid),www-data(groupid) というユーザ権限で動作している。そして、webサーバと連動して動く PHP のプログラムも www-data の権限で動作する。一方で、通常ユーザが開発しているプログラムが置かれる $HOME/public_html フォルダは、何もしなければそのユーザのものである。このため、PHP のプログラムがユーザのフォルダ内にアクセスする際には、www-data に対してのアクセス権限が必要となる。
Windows ユーザが Web プログラミングの体験をする際には、XAMPP などのパッケージを利用することも多いだろう。しかし XAMPP などは、中身のWebサーバ(apache), DBサーバ(MySQL)などすべてがインストールしたユーザ権限で動いたりするため、所有権の設定の知識が無くても簡単に利用することができる(あるいはユーザ自身が管理者権限を持っているため設定が無くてもアクセス権問題が発生しない)。このため Linux 環境での Web プログラミングに移行する際に、ユーザ権限の設定を忘れ、プログラムが動かず戸惑うことも多い。
今回の演習では、XAMPPを使わず、演習用サーバを用いる。
データベースサーバの場合
また、データの保存でデータベースを利用する場合、Oracle や MySQL といった、ネットワーク型のデータベースでは、Webサーバとは別にデータベースのサーバプログラムが動作している。ネットワーク型のデータベースでは、様々なユーザ・アプリケーションがデータの読み書きを行うため、SQL の create user 命令でユーザを割り当て、grant 命令でユーザのデータへのアクセス権限を指定する。
簡易データベースSQLiteの場合
簡単なデータベースシステムの SQLite は、PHP の SQLite プラグインを経由してディレクトリ内のファイルにアクセスする。このため、データベースファイルやデータベースのファイルが置かれているフォルダへのアクセス権限が必要となる。今回の演習用サーバでは、ゲストアカウントは www-data グループに所属しているので、データベースファイルやフォルダに対し、www-data グループへの書き込み権限を与える。
chown , chgrp , chmod コマンド
ファイル所有者やグループを変更する場合には、chown (change owner) 命令や chgrp (change group) 命令を使用する。
chown ユーザID ファイル 例: $ chown tsaitoh helloworld.c chgrp グループID ファイル 例: $ chgrp www-data public_html
ファイルに対するパーミッション(利用権限)を変更するには、chmod (change mode) 命令を用いる。
chmod 命令では、読み書きの権限は2進数3桁の組み合わせで扱う。読書可 “rw-“ = 6, 読出可 = “r–“ = 4 , ディレクトリの読み書き可 “rwx” = 7 など。ファイルには、所有者,グループ,その他の3つに分けて、読み書きの権限を割り当てる。2進数3桁=8進数1桁で表現できることから、一般的なファイルの “rw-,r–,r–“ は、8進数3桁 で 644 , ディレクトリなら “rwx,r-x,r-x” は 755 といった値で表現する。
chmod 権限 ファイル 例: $ chmod 664 helloworld.c $ ls -al -rw-rw-r-- tsaitoh ei 123 5月20 12:34 helloworld.c $ chmod 775 public_html drwxrwxr-x tsaitoh www-data 4096 5月20 12:34 public_html 8進数表現を使わない場合 $ chmod u+w,g+w helloworld.c ユーザ(u)への書き込み権限,グループ(g)への書き込み権限の追加(+) $ chmod g-w,o-rw helloworld.c グループ(g)書き込み権限を消す、その他(o)の読み書き権限を消す(-) $ chmod u=rw,g=r,o=r helloworld.c ユーザ(u)への読み書き,グループ(g),その他(o)への読み出し権限を設定(=)
演習内容
前回の演習と同じ方法でサーバにログインし、サーバ上で直接ファイル編集をしてみよう。
C:\Users\tsaitoh> ssh -P 443 guest00@nitfcei.mydns.jp $ ls -al -rw-r--r-- 1 guest00 root 76 Mar 8 12:06 helloworld.c $ vi helloworld.c もしくは $ emacs helloworld.c
- vim の使い方
- 挿入 iテキストESC
削除 x
ファイルの保存 :w
エディタの修了 ZZ
- emacs の使い方
- ファイルの保存 Ctrl-X Ctrl-S
エディタの修了 Ctrl-X Ctrl-C
GitHubから演習ファイルを複製
GitHub は、複数の開発者が共同でプログラムを開発するための環境で、プログラムの情報共有などに広く使われている。ファイルは、git コマンドで複製や更新ができる。
(( public_html の中に演習用ファイルを github からダウンロード )) $ cd ~/public_html public_html$ git clone https://github.com/tohrusaitoh/recp.git public_html/recp$ cd recp/ public_html/recp$ ls -al -rw-r--r-- 1 t-saitoh home 870 11月 10 2021 Makefile -rw-r--r-- 1 t-saitoh home 1152 10月 8 2021 README.md :
サーバ上のファイルをパソコンにコピーして編集
(( サーバ上のファイル sampleI.php (sample-アイ.php) をダウンロード )) C:\Users\tsaitoh> scp -P 443 guest00@nitfcei.mydns.jp:public_html/recp/sampleI.php . VSCode などのエディタで編集 (( 編集した sampleI.php をサーバにアップロード )) C:\Users\tsaitoh> scp -P 443 sampleI.php guest00@nitfcei.mydns.jp:public_html/recp/
Webサーバで書き込みができるように設定
(( public_html のデータベースファイル shopping.db を書き込み可能にする )) $ chgrp www-data ~guest00/public_html/recp/shopping.db $ chmod g+w ~guest00/public_html/recp/shopping.db (( public_html/recp フォルダを書き込み可能にする )) $ chgrp www-data ~guest00/public_html/recp $ chmod g+w ~guest00/public_html/recp
バックエンドプログラムを実行してみる
パソコンのブラウザで、http://nitfcei.mydns.jp/~guest00/recp/sampleI.php を開く。
書き込み結果を確認してみる
(( データベースファイル shopping.db の書込み結果を確認 )) $ cd ~guest00/public_html/recp public_html/recp$ sqlite3 shopping.db SQLite version 3.31.1 2020-01-27 19:55:54 Enter ".help" for usage hints. sqlite> select * from BUYLIST ; 1010|10001|2021-11-05|1 1020|10001|2021-11-05|2 1022|10001|2021-11-05|3 : sqlite> [Ctrl-D] コントロールDで sqlite3 を抜ける public_html/recp$
レポート課題(sampleI.phpの修正と動作確認)の提出先※ 作業のみでレポートはなし
派生と継承
隠ぺい化の次のステップとして、派生・継承を説明する。オブジェクト指向プログラミングでは、一番基本となるデータ構造を宣言し、その基本構造に様々な機能を追加した派生クラスを記述することでプログラムを作成する。今回は、その派生を理解するためにC言語で発生する問題点を考える。
派生を使わずに書くと…
元となるデータ構造(例えばPersonが名前と年齢)でプログラムを作っていて、 途中でその特殊パターンとして、所属と学年を加えた学生(Student)という データ構造を作るとする。
// 元となる構造体(Person) / 基底クラス struct Person { char name[ 20 ] ; // 名前 int age ; // 年齢 } ; // 初期化関数 void set_Person( struct Person* p , char s[] , int x ) { strcpy( p->name , s ) ; p->age = x ; } // 表示関数 void print_Person( struct Person* p ) { printf( "%s %d\n" , p->name , p->age ) ; } int main() { struct Person saitoh ; set_Person( &saitoh , "t-saitoh" , 50 ) ; print_Person( &saitoh ) ; return 0 ; }
パターン1(そのまんま…)
上記のPersonに、所属と学年を加えるのであれば、以下の方法がある。 しかし以下パターン1は、要素名がname,ageという共通な部分があるようにみえるが、 プログラム上は、PersonとPersonStudent1は、まるっきり関係のない別の型にすぎない。
このため、元データと共通部分があっても、同じ処理を改めて書き直しになる。(プログラマーの手間が減らせない)
// 元のデータに追加要素(パターン1) struct PersonStudent1 { // Personと同じ部分 char name[ 20 ] ; // 名前 int age ; // 年齢 // 追加部分 char dep[ 20 ] ; // 所属 int grade ; // 学年 } ; void set_PersonStudent1( struct PersonStudent1* p , char s[] , int x , char d[] , int g ) { // set_Personと同じ処理を書いている。 strcpy( p->name , s ) ; p->age = x ; // 追加された処理 strcpy( p->dep , d ) ; p->grade = g ; } // 名前と年齢 / 所属と学年を表示 void print_PersonStudent1( struct PersonStudent1* p ) { // print_Personと同じ処理を書いている。 printf( "%s %d\n" , p->name , p->age ) ; printf( "- %s %d¥n" , p->dep , p->grade ) ; } int main() { struct PersonStudent1 yama1 ; set_PersonStudent1( &yama1 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent1( &yama1 ) ; return 0 ; }
パターン2(元データの処理を少し使って…)
パターン1では、機能が追加された新しいデータ構造のために、同じような処理を改めて書くことになりプログラムの記述量を減らせない。面倒なので、 元データ用の関数をうまく使うように書いてみる。
// 元のデータに追加要素(パターン2) struct PersonStudent2 { // 元のデータPerson struct Person person ; // 追加部分 char dep[ 20 ] ; int grade ; } ; void set_PersonStudent2( struct PersonStudent2* p , char s[] , int x , char d[] , int g ) { // Personの関数を部分的に使う set_Person( &(p->person) , s , x ) ; // 追加分はしかたない strcpy( p->dep , d ) ; p->grade = g ; } void print_PersonStudent2( struct PersonStudent2* p ) { // Personの関数を使う。 print_Person( &p->person ) ; printf( "- %s %d¥n" , p->dep , p->grade ) ; } int main() { struct PersonStudent2 yama2 ; set_PersonStudent2( &yama2 , "yama" , 22 , "PS" , 2 ) ; print_PersonStudent2( &yama2 ) ; return 0 ; }
このパターン2であれば、元データ Person の処理をうまく使っているので、 プログラムの記述量を減らすことはできるようになった。
しかし、print_PersonStudent2() のような処理は、名前と年齢だけ表示すればいいという場合、元データ構造が同じなのに、 PersonStudent2 用のプログラムをいちいち記述するのは面倒ではないか?
そこで、元データの処理を拡張し、処理の流用ができないであろうか?
基底クラスから派生クラスを作る
オブジェクト指向では、元データ(基底クラス)に新たな要素を加えたクラス(派生クラス)を 作ることを「派生」と呼ぶ。派生クラスを定義するときは、クラス名の後ろに、 「:」,「public/protected/private」, 基底クラス名を書く。
// 基底クラス class Person { private: char name[ 20 ] ; int age ; public: Person( const char s[] , int x ) : age( x ) { strcpy( name , s ) ; } void print() { printf( "%s %d\n" , name , age ) ; } } ; // 派生クラス(Student は Person から派生) class Student : public Person { private: // 追加部分 char dep[ 20 ] ; int grade ; public: Student( const char s[] , int x , const char d[] , int g ) : Person( s , x ) // 基底クラスのコンストラクタ { // 追加された処理 strcpy( dep , d ) ; grade = g ; } } ; int main() { Person saitoh( "t-saitoh" , 50 ) ; saitoh.print() ; Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; // "yama 22"が表示される return 0 ; }
ここで注目すべき点は、main()の中で、Studentクラス”yama”に対し、yama.print() を呼び出しているが、パターン2であれば、print_PersonStudent2()に相当するプログラムを 記述していない。 しかし、この派生を使うと Person の print() が自動的に流用することができる。 これは、基底クラスのメソッドを「継承」しているから、 このように書け、名前と年齢「yama 22」が表示される。
さらに、Student の中に、以下のような Student 専用の新しい print()を記述してもよい。
class Student ...略... { ...略... // Student クラス専用の print() void print() { // 親クラス Person の print() を呼び出す Person::print() ; // Student クラス用の処理 printf( "%s %d\n" , dep , grade ) ; } } ; void main() { ...略... Student yama( "yama" , 22 , "PS" , 2 ) ; yama.print() ; }
この場合は、継承ではなく機能が上書き(オーバーライト)されるので、 「yama 22 / PS 2」が表示される。
派生クラスを作る際の後ろに記述した、public は、他にも protected , private を 記述できる。
public だれもがアクセス可能。 protected であれば、派生クラスからアクセスが可能。 派生クラスであれば、通常は protected で使うのが一般的。 private 派生クラスでもアクセス不可。
C言語で無理やりオブジェクト指向の”派生”を使う方法
オブジェクト指向の機能の無いC言語で、このような派生と継承を実装する場合には、共用体を使う以下のようなテクニックが使われていた。
unix の GUI である X11 でも共用体を用いて派生を実装していた。// 基底クラス struct PersonBase { // 基底クラス char name[ 20 ] ; int age ; } ; struct PersonStudent { // 派生クラス struct PersonBase base ; char dep[ 20 ] ; int grade ; } ; //(base) //(student) union Person { // name //[name] struct PersonBase base ; // age //[age ] struct PersonStudent student ; // dep } ; // grade void person_Print( struct Person* p ) { printf( "%s %d\n" , p->base.name , p->base.age ) ; } int main() { struct PersonBase tsaitoh = { "tsaitoh" , 55 } ; struct PersonStudent mitsuki = { { "mitsuki" , 21 } , "KIT" , 4 } ; print_Person( (struct Person*)&tsaitoh ) ; print_Person( (struct Person*)&mitsuki ) ; // 無理やり print_Person を呼び出す return 0 ; }
仮想関数への伏線
上記のような派生したプログラムを記述した場合、以下のようなプログラムでは何が起こるであろうか?
class Student ... { : void print() { Person::print() ; // 名前と年齢を表示 printf( " %s %d¥n" , dep , grade ) ; // 所属と学年を表示 } } ; int main() { Person saitoh( "t-saitoh" , 55 ) ; saitoh.print() ; // t-saitoh 55 名前と年齢を表示 Student mitsu( "mitsuki" , 20 , "KIT" , 3 ) ; Student ayuka( "ayuka" , 18 , "EI" , 4 ) ; mitsu.print() ; // mitsuki 20 / KIT 3 名前,年齢,所属,学年を表示 ayuka.print() ; // ayuka 18 / EI 4 名前,年齢,所属,学年を表示 Person* family[] = { &saitoh , &mitsu , &ayuka , // 配列の中に、Personへのポインタと } ; // Studentへのポインタが混在している // 派生クラスのポインタは、 // 基底クラスのポインタとしても扱える for( int i = 0 ; i < 3 ; i++ ) family[ i ]->print() ; // t-saitoh 55/mitsuki 20/ayuka 18 return 0 ; // が表示される。 } // # "mitsuki 20/KIT 3" とか "ayuka 18/EI 4" // # が表示されてほしい?
unixにおけるファイルとユーザ管理
Unix演習サーバへの接続
Unix(Linux)は、インターネットでのサーバとして広く活用されている。Linuxを試すには、Windows ならば WSL や Cygwin であったり、Mac でも使える仮想OSの VMware, VirrtualBox を使うこともできる。今回の演習では、全員が同じ環境で使うために、クラウド環境にサーバを準備し利用する。
ネットワークの向こう側にあるサーバを利用する場合、以下のような方法が使われる。
- telnet (port 23)
- キー入力を相手に送って、送られてくるデータを画面に表示する。
- 通信データが暗号化されないので盗聴される心配があり、一般的には使用しない。
- rsh (remote shell – port 514)
- ネットワークを越えてコマンドを実行したりファイル転送ができる。
- telnet 同様に暗号化されていないので、次に示す ssh を使うのが一般的。
- ssh (secure shell – port 22)
- rsh の処理を暗号化しながら実行。
- ネットワークを越えた処理を行う際の基本だが、ssh を経由した攻撃が多いことから、通常のポート番号22以外を使ったり、アクセス制限を厳しく設定する必要がある。
- remote Desktop
- ネットワークの先のPCの画面をネットワーク越しに触れるようにしたもの。
教室のWiFi環境(fnct-student)では、HTTP(80) , HTTPS(443) の通信しか使えないことから、ssh(22) が通常利用できない。電子情報のWiFiアクセスポイント(nitfc-ei-student等)であれば、ssh などが使用できる。
今回授業の演習では、さくらインターネットのサーバ上のクラウドサーバを利用する。
ただし、さくらインターネットのクラウドサーバでは、ssh(port=22)が使用できるが、ssh 接続の際にログインパスワードの間違いなどが多発すると、ssh 経由の攻撃の可能性があると判断され、ssh(port=22)接続が一定時間使えなくなる対策がとられている。今回は、ゲストアカウントでパスワード入力ミスが多発することが想定されるので、port=22のsshは使用しない。
リモート接続を行う
Windows 10 or Windows 11 ならば、cmd.exe , macOS ならば、ターミナルソフトを起動し、以下の操作を行う。
$ ssh -p 443 ゲストID@演習サーバ
- 443ポートは通常は https 用だが、今回はサーバで ssh プロトコルを 443 ポートで受け付けるように設定してある。かなり特殊な使い方なので要注意。
- 演習サーバの接続方法(学内のみ) – サーバへの攻撃を極力へらすために非公開。
- 今回の演習では、センターIDではなくゲストIDを使います。
- ゲストIDのパスワードは、こちらのファイル(Teams)を参照。(2023-4EI Teams)
- パスワード入力時は、打つたびに●●●といった文字は表示されません。
- パスワード入力時にタイプミスした時は、Ctrl-U で最初から入力のやり直しができます。
ファイル操作の基本
まずは基本操作をしてみよう。ls コマンド(list) は、ディレクトリ内にあるファイルの一覧を表示する。cat コマンド(catalog)は、指定されたファイルの内容を表示する。
s53599xx@nitfcei:~$ ls helloworld.c Maildir public_data public_html s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir (略) s53599xx@nitfcei:~$ cat helloworld.c #include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; } s53599xx@nitfcei:~$
ファイルをコピーするには cp コマンド(copy)、不要なファイルを消すには rm コマンド(remove)を使う。
s53599xx@nitfcei:~$ cp helloworld.c test.c s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir -rw-r--r-- 1 s53599xx students 76 Dec 21 14:40 test.c (略) s53599xx@nitfcei:~$ rm test.c s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir s53599xx@nitfcei:~$
ファイル詳細表示の説明
ls -l で表示される詳細の内容は以下の通り。
属性 | リンク数 | 所有者 | グループ | サイズ | 日付 | ファイル名 |
---|---|---|---|---|---|---|
– rw- r– r– | 1 | s53599xx | students | 76 | Dec 21 14:30 | helloworld.c |
d rwx — — | 5 | s53599xx | students | 4096 | Dec 21 14:30 | Maildir |
– | d | -: 通常ファイル, d:ディレクトリ | ||||
rw- | r,w,x | 所有者が r:読み出し, w:書き込み, -: 権限なし ファイルなら、x:実行可能 ディレクトリなら、x:ディレクトリに入れる |
||||
r – – | – – – | グループの rwx の属性 r– は 読み込みだけ許可 | ||||
r – – | – – – | その他の rwx の属性 — は、読み書き禁止 |
基本的なファイル操作コマンド一覧
操作 | Linux | Windows |
---|---|---|
ディレクトリ一覧(list) ディレクトリ詳細 |
ls 場所 ※ ls -l 場所 |
dir /w 場所 ※ dir 場所 |
※ 省略時はカレントディレクトリ | ||
ファイル表示(catalog) | cat 場所 | type 場所 |
ファイルコピー(copy) | cp コピー元 コピー先 cp コピー元 コピー先ディレクトリ |
copy コピー元 コピー先 |
ファイル削除(remove) | rm 場所 | del 場所 |
ディレクトリ作成(make dir) | mkdir 場所 | md 場所 |
ディレクトリ削除(remove dir) | rmdir 場所 | rmdir 場所 |
カレントディレクトリ移動 (change directory) |
cd 場所 | cd 場所 ドライブの場合は ドライブ名: |
所有者を変更(change owner) | chown 所有者 場所 | |
グループを変更(change group) | chgrp グループ 場所 | |
属性を変更(change mode) | chmod 属性 場所 | ←属性の書き方 |
ワイルドカード文字
ls などのコマンドで、複数のファイルを対象とするとき、ワイルドカード文字が使える。
任意の1文字 ? |
(例) $ ls # 全部のファイル aaa.c ab.c abc.c bcd.c defgh.c hij.cxx $ ls a?.c # aで始まる2文字のC言語ファイル ab.c $ ls ???.c # 3文字のC言語のファイル aaa.c abc.c bcd.c |
任意の文字 * |
(例) $ ls a*.c # aで始まるC言語ファイル aaa.c ab.c abc.c $ ls *.cxx # 拡張子が.cxxのファイル(C++) hij.cxx |
相対PATHと絶対PATH
ファイルの場所を指定するには、2つの方法がある。
絶対PATHは、木構造の根(ルートディレクトリ / で表す) からの経路のディレクトリ名を”/”で区切って書き連ねる。ルートディレクトリからの場所であることを示すために、先頭を / で始める。住所を /福井県/越前市/宮谷町/斉藤家 と書くようなもの。
相対PATHは、現在注目しているディレクトリ(カレントディレクトリと呼ぶ)からの経路を書く。住所でいうと、/福井県/越前市 に注目している状態で、宮谷町/斉藤家 と書くようなもの。
ただし、/福井県/福井市 に注目している状態で、片町/山本家 は1つのファイルでも、/福井県/福井市/片町/山本家 とは別に /石川県/金沢市/片町/山本家 があるかもしれない。
上記の絵であれば、/home/tsaitoh/helloworld.c を、相対PATHで書く場合、s53599xx の一つ上にさかのぼって場所を指定することもできる。一つ上のディレクトリ(親ディレクトリ)は .. (ピリオド2つ)
この場合、” $ cat ../tsaitoh/helloworld.c ” の様な相対PATHでもアクセスできる。
カレントディレクトリ自身を表す場合は、. (ピリオド1つ)を使う。
/home/s53599xx/helloworld.c の場所は、” $ cat ./helloworld.c ” と書くこともできる。
ユーザとグループ
unixでは、ユーザとグループでアクセス制限をすることができる。ユーザ情報は、/etc/passwd ファイルで確認できる。グループ情報は、/etc/group ファイルで確認できる。
$ more /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin (略) guest00:x:1200:1200:guest00,,,:/home0/guests/guest00:/bin/bash $ more /etc/group root:x:0: daemon:x:1: bin:x:2: (略) guests:x:1200:guest00,guest01,guest02,...
/etc/passwd | /etc/group |
guest00 — ユーザID x — 昔は暗号化されたパスワード 1200 — ユーザID番号 1200 — グループID番号(/etc/groupを参照) guest00,,, — ユーザの正式名や電話番号など /home0/guests/guest00 — ホームディレクトリ /bin/bash — 使用する shell |
guests — グループID x — 昔は暗号化されたグループパスワード 1200 — グループID番号 guest00,guest01,guest02 — 所属するユーザ一覧 |
アクセス制限の実験
/home0/Challenge/AccesControl に、いくつかのファイルが保存してあり、t-saitoh が見ると、以下のようなファイルであった。tree コマンドでは、いくつかのディレクトリとその中のファイルが確認できる。しかし、ls -al にてファイルのアクセス権限が確認できる。tree コマンドで確認できるファイルにアクセスすると何が起こるか確認すること。
$ cd /home0/Challenge/AccessControl $ id # 自分のID,グループを確認 uid=1200(guest00) gid=1200(guests) groups=1200(guests) $ tree # ディレクトリ構造を表示 $ ls -al # 権限情報を表示 |
|
![]() |
![]() |
Windows とアクセスコントロール
Unix のシステムでは、ファイル毎に、ユーザID,グループIDを割り当て、ユーザ, グループ, その他に対して、Read, Write などの制限をかける。Windows では、さらに細かくアクセス制限を加えることができる。Windows では、1つのファイルに対して、ユーザやグループのRead/Writeなどの制限をいくつでも設定できる。Access Control List と呼ばれる。
主要なディレクトリとファイルシステム
unix では、すべてのデバイスを / (ルートディレクトリ) 配下に木構造につなげて管理している。CD-ROM や USB ディスクなどは、指定したディレクトリに mount (マウント) して使用する。
ext4 は、Linux で採用されているファイルシステムで、システムの保存に使われる。
tmpfs は、主記憶(D-RAM) の一部を、ディスクと同じように扱えるようにしたファイルシステム。通称 ram disk(ラムディスク)。保存はメモリへのアクセスなので、保存やアクセスは極めて高速だが、保存領域は少ない。高速に扱えて、システムが再起動された時に消えても問題のない情報を保存するために使われる。
proc は、実行中のプロセス情報を、ハードディスクに保存されたファイルの様に参照できる。
vfat , exfat は、USBメモリ, SDカード のデータ保存で使われるファイルシステムで、Windows(MS-DOS) で使われている保存形式。ファイルにファイル所有者などの概念がない。
ntfs は、Windows で使われているファイル形式。
swap は、仮想メモリのためのデータが保存される。主記憶メモリが不足した際に、使用頻度の少ないメモリ領域をハードディスクに保存するための領域。以下のような free コマンドで使用状況が確認できる。一般的に、主記憶メモリの数倍を割り当てる。
DCON2024 本選にて4EIチームが企業賞
作品イメージ
(2024/5/20追記) 企業賞 QUICK さまの副賞を頂きました
(2024/5/21追記) DCON副賞 エナジードリンクが届きました。
ポインタと文字列処理
C言語でのポインター
#include <stdio.h> int main() { int x = 123 ; // px [ 〇 ] int* px ; // px はポインタ ↓ px = &x ; // x の変数の番地を px に代入 x [ 123 ] *px = 321 ; // px の指し示す場所に 321 を代入 printf( "%d\n" , x ) ; // 321 を出力 return 0 ; }
値渡し(pass by value)
// 値渡しのプログラム void foo( int x ) { // x は局所変数(仮引数は呼出時に // 対応する実引数で初期化される。 x++ ; printf( "%d¥n" , x ) ; } int main() { int a = 123 ; foo( a ) ; // 124 // 処理後も main::a は 123 のまま。 foo( a ) ; // 124 return 0 ; }
このプログラムでは、aの値は変化せずに、124,124 が表示される。
でも、プログラムによっては、124,125 と変化して欲しい場合もある。
どのように記述すべきだろうか?
// 大域変数を使う場合 int x ; void foo() { x++ ; printf( "%d¥n" , x ) ; } int main() { x = 123 ; foo() ; // 124 foo() ; // 125 return 0 ; }
しかし、このプログラムは大域変数を使うために、間違いを引き起こしやすい。
// 大域変数が原因で予想外の挙動をしめす簡単な例 int i ; void foo() { for( i = 0 ; i < 2 ; i++ ) printf( "A" ) ; } int main() { for( i = 0 ; i < 3 ; i++ ) // このプログラムでは、AA AA AA と foo() ; // 表示されない。 return 0 ; }
ポインタ渡し(pass by pointer)
C言語で引数を通して、呼び出し側の値を変化して欲しい場合は、変更して欲しい変数のアドレスを渡し、関数側では、ポインタ変数を使って受け取った変数のアドレスの示す場所の値を操作する。
// ポインタ渡しのプログラム void foo( int* p ) { // p はポインタ (*p)++ ; printf( "%d¥n" , *p ) ; } int main() { int a = 123 ; foo( &a ) ; // 124 // 処理後 main::a は 124 に増えている。 foo( &a ) ; // 124 return 0 ; // さらに125と増える }
ポインタを利用して引数に副作用を与える方法は、ポインタを正しく理解していないプログラマーでは、危険な操作となる。C++では、ポインタ渡しを極力使わないようにするために、参照渡しを利用する。ただし、ポインタ渡しも参照渡しも、機械語レベルでは同じ処理にすぎない。
参照渡し(pass by reference)
// ポインタ渡しのプログラム void foo( int& x ) { // xは参照 x++ ; printf( "%d¥n" , x ) ; } int main() { int a = 123 ; foo( a ) ; // 124 // 処理後 main::a は 124 に増えている。 foo( a ) ; // 124 return 0 ; // さらに125と増える。 }
ポインタの加算と配列アドレス
ポインタに整数値を加えることは、アクセスする場所が、指定された分だけ後ろにずれることを意味する。
// ポインタ加算の例 int a[ 5 ] = { 11 , 22 , 33 , 44 , 55 } ; void main() { int* p ; // p∇ p = &a[2] ; // a[] : 11,22,33,44,55 // -2 +0 +1 printf( "%d¥n" , *p ) ; // 33 p[0] printf( "%d¥n" , *(p+1) ) ; // 44 p[1] printf( "%d¥n" , *(p-2) ) ; // 11 p[-2] p = a ; // p∇ printf( "%d¥n" , *p ) ; // a[] : 11,22,33,44,55 p++ ; // → p∇ printf( "%d¥n" , *p ) ; // a[] : 11,22,33,44,55 p += 2 ; // → → p∇ printf( "%d¥n" , *p ) ; // a[] : 11,22,33,44,55 }
ここで、注意すべき点は、ポインタの加算した場所の参照と、配列の参照は同じ意味となる。
*(p + 整数式) と p[ 整数式 ] は同じ意味 (参照”悪趣味なプログラム”)
特に配列 a[] の a だけを記述すると、配列の先頭を意味することに注意。
ポインタと文字列処理
#include <stdio.h> void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '#include <stdio.h> void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } int main(void){ char str[ 20 ] ; my_tolower( str , "AaBcDeF Hoge" ) ; printf( "%s\n" , str ) ; return 0 ; }' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '#include <stdio.h> void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } int main(void){ char str[ 20 ] ; my_tolower( str , "AaBcDeF Hoge" ) ; printf( "%s\n" , str ) ; return 0 ; }' ; } int main(void){ char str[ 20 ] ; my_tolower( str , "AaBcDeF Hoge" ) ; printf( "%s\n" , str ) ; return 0 ; }
間違ったプログラム
C言語の面倒な点は、データがどのように格納されるのかを考えないと正しく動かない所であろう。
下記のプログラムの問題点がわかるだろうか?
#include <stdio.h> // 前述の my_tolower と同じ void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '#include <stdio.h> // 前述の my_tolower と同じ void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } // 引数に副作用のある my_tolower char* my_tolower_1( char s[] ) { for( int i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) s[i] = s[i] - 'A' + 'a' ; return s ; } // 局所変数のメモリを帰してはダメ char* my_tolower_2( char s[] ) { char str[ 20 ] ; int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) str[i] = s[i] - 'A' + 'a' ; else str[i] = s[i] ; str[i] = '\0' ; // printf( "in my_tolower_2 : %s\n" , str ) ; return str ; } int main(void) { char str[ 20 ] = "Hoge" ; ; // case-1 char* ptr ; my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped) // case-2 printf( "%s\n" , my_tolower_1( str ) ) ; printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない // csse-3 printf( "%s\n" , my_tolower_2( "foo" ) ) ; // ゴミが表示される return 0 ; }' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '#include <stdio.h> // 前述の my_tolower と同じ void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } // 引数に副作用のある my_tolower char* my_tolower_1( char s[] ) { for( int i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) s[i] = s[i] - 'A' + 'a' ; return s ; } // 局所変数のメモリを帰してはダメ char* my_tolower_2( char s[] ) { char str[ 20 ] ; int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) str[i] = s[i] - 'A' + 'a' ; else str[i] = s[i] ; str[i] = '\0' ; // printf( "in my_tolower_2 : %s\n" , str ) ; return str ; } int main(void) { char str[ 20 ] = "Hoge" ; ; // case-1 char* ptr ; my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped) // case-2 printf( "%s\n" , my_tolower_1( str ) ) ; printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない // csse-3 printf( "%s\n" , my_tolower_2( "foo" ) ) ; // ゴミが表示される return 0 ; }' ; } // 引数に副作用のある my_tolower char* my_tolower_1( char s[] ) { for( int i = 0 ; s[i] != '#include <stdio.h> // 前述の my_tolower と同じ void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } // 引数に副作用のある my_tolower char* my_tolower_1( char s[] ) { for( int i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) s[i] = s[i] - 'A' + 'a' ; return s ; } // 局所変数のメモリを帰してはダメ char* my_tolower_2( char s[] ) { char str[ 20 ] ; int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) str[i] = s[i] - 'A' + 'a' ; else str[i] = s[i] ; str[i] = '\0' ; // printf( "in my_tolower_2 : %s\n" , str ) ; return str ; } int main(void) { char str[ 20 ] = "Hoge" ; ; // case-1 char* ptr ; my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped) // case-2 printf( "%s\n" , my_tolower_1( str ) ) ; printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない // csse-3 printf( "%s\n" , my_tolower_2( "foo" ) ) ; // ゴミが表示される return 0 ; }' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) s[i] = s[i] - 'A' + 'a' ; return s ; } // 局所変数のメモリを帰してはダメ char* my_tolower_2( char s[] ) { char str[ 20 ] ; int i ; for( i = 0 ; s[i] != '#include <stdio.h> // 前述の my_tolower と同じ void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } // 引数に副作用のある my_tolower char* my_tolower_1( char s[] ) { for( int i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) s[i] = s[i] - 'A' + 'a' ; return s ; } // 局所変数のメモリを帰してはダメ char* my_tolower_2( char s[] ) { char str[ 20 ] ; int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) str[i] = s[i] - 'A' + 'a' ; else str[i] = s[i] ; str[i] = '\0' ; // printf( "in my_tolower_2 : %s\n" , str ) ; return str ; } int main(void) { char str[ 20 ] = "Hoge" ; ; // case-1 char* ptr ; my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped) // case-2 printf( "%s\n" , my_tolower_1( str ) ) ; printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない // csse-3 printf( "%s\n" , my_tolower_2( "foo" ) ) ; // ゴミが表示される return 0 ; }' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) str[i] = s[i] - 'A' + 'a' ; else str[i] = s[i] ; str[i] = '#include <stdio.h> // 前述の my_tolower と同じ void my_tolower( char d[] , char s[] ) { int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) d[i] = s[i] - 'A' + 'a' ; else d[i] = s[i] ; d[i] = '\0' ; } // 引数に副作用のある my_tolower char* my_tolower_1( char s[] ) { for( int i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) s[i] = s[i] - 'A' + 'a' ; return s ; } // 局所変数のメモリを帰してはダメ char* my_tolower_2( char s[] ) { char str[ 20 ] ; int i ; for( i = 0 ; s[i] != '\0' ; i++ ) if ( 'A' <= s[i] && s[i] <= 'Z' ) str[i] = s[i] - 'A' + 'a' ; else str[i] = s[i] ; str[i] = '\0' ; // printf( "in my_tolower_2 : %s\n" , str ) ; return str ; } int main(void) { char str[ 20 ] = "Hoge" ; ; // case-1 char* ptr ; my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped) // case-2 printf( "%s\n" , my_tolower_1( str ) ) ; printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない // csse-3 printf( "%s\n" , my_tolower_2( "foo" ) ) ; // ゴミが表示される return 0 ; }' ; // printf( "in my_tolower_2 : %s\n" , str ) ; return str ; } int main(void) { char str[ 20 ] = "Hoge" ; ; // case-1 char* ptr ; my_tolower( ptr , "Piyo" ) ; // Illegal instruction (core dumped) // case-2 printf( "%s\n" , my_tolower_1( str ) ) ; printf( "%s\n" , my_tolower_1( "Fuga" ) ) ; // 小文字にならない // csse-3 printf( "%s\n" , my_tolower_2( "foo" ) ) ; // ゴミが表示される return 0 ; }
ポインタインクリメントと式
C言語では、ポインタを動かしながら処理を行う場合に以下のようなプログラムもよくでてくる。
// string copy 配列のイメージで記載 void strcpy( char d[] , char s[] ) { int i ; for( i = 0 ; s[ i ] != '// string copy 配列のイメージで記載 void strcpy( char d[] , char s[] ) { int i ; for( i = 0 ; s[ i ] != '\0' ; i++ ) d[ i ] = s[ i ] ; d[ i ] = '\0' ; } int main() { char a[] = "abcde" ; char b[ 10 ] ; strcpy( b , a ) ; printf( "%s\n" , b ) ; return 0 ; }' ; i++ ) d[ i ] = s[ i ] ; d[ i ] = '// string copy 配列のイメージで記載 void strcpy( char d[] , char s[] ) { int i ; for( i = 0 ; s[ i ] != '\0' ; i++ ) d[ i ] = s[ i ] ; d[ i ] = '\0' ; } int main() { char a[] = "abcde" ; char b[ 10 ] ; strcpy( b , a ) ; printf( "%s\n" , b ) ; return 0 ; }' ; } int main() { char a[] = "abcde" ; char b[ 10 ] ; strcpy( b , a ) ; printf( "%s\n" , b ) ; return 0 ; }
しかし、この strcpy は、ポインタを使って書くと以下のように書ける。
// string copy ポインタのイメージで記載 void strcpy( char* p , char* q ) { while( *q != '// string copy ポインタのイメージで記載 void strcpy( char* p , char* q ) { while( *q != '\0' ) { *p = *q ; p++ ; q++ ; } *p = '\0' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '\0' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '\0' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ) { *p = *q ; p++ ; q++ ; } *p = '// string copy ポインタのイメージで記載 void strcpy( char* p , char* q ) { while( *q != '\0' ) { *p = *q ; p++ ; q++ ; } *p = '\0' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '\0' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '\0' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '// string copy ポインタのイメージで記載 void strcpy( char* p , char* q ) { while( *q != '\0' ) { *p = *q ; p++ ; q++ ; } *p = '\0' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '\0' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '\0' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '// string copy ポインタのイメージで記載 void strcpy( char* p , char* q ) { while( *q != '\0' ) { *p = *q ; p++ ; q++ ; } *p = '\0' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '\0' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '\0' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '// string copy ポインタのイメージで記載 void strcpy( char* p , char* q ) { while( *q != '\0' ) { *p = *q ; p++ ; q++ ; } *p = '\0' ; } // ポインタ加算と代入を一度に書く void strcpy( char* p , char* q ) { while( *q != '\0' ) *p++ = *q++ ; // *(p++) = *(q++) *p = '\0' ; } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ) // while( *p++ = *q++ ) ; でも良い ; }