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++ ) ; でも良い ; }
フローチャートと整数型
学際科目の情報制御基礎において、プログラムの基本としてフローチャートと基本的な処理を説明し、数値型の注意点を説明。
フローチャートの基本
プログラムの処理の順序を理解するには、初心者であればフローチャート(流れ図)を使う。
処理の1つ1つを箱で表し、流れを箱の間の矢印で示すことでアルゴリズム(プログラムの考え方)や処理順序を表現する。処理単位の箱は、命令の種類によって箱の書き方が決まっている。
上図右側のフローチャートの例では、以下の説明のように実行され、0,1,2,…,9 が表示され、最終的に変数 i が10以上になり処理を停止する。
(1) 変数 i に 0 を保存
(2) 変数 i は10未満なら(3)、10以上なら終了
(3) 変数 i を表示
(4) i = i + 1 右辺の計算結果を、左辺に代入。iが0から1に変化
(5) 処理(2)から繰り返し。
上記のようなプログラムは、C言語であれば以下のようになる。
#include <stdio.h> | 入出力関数を使うための準備 int main() { | 最初に main() という関数が呼び出される。 int i ; | 変数 i の入れ物を準備 for( i = 0 ; i < 10 ; i++ ) { | 最初に i = 0 を行い、i < 10 の条件を満たす間繰り返し、 | 繰り返しの度に i を1つ増やす printf( "%d\n" , i ) ; | i の値を表示 } return 0 ; | 正しく終わったら0を返す。 }
練習問題1
以下のフローチャートの処理A,処理B,処理C,処理Dの実行結果を答えよ。
- 電気電子工学科,電子情報工学科の学生は、出席番号が偶数は処理C,奇数は処理Dについて回答せよ。
- それ以外の学科の学生は、出席番号が偶数は処理A,奇数は処理Bの結果について回答せよ。
- このプログラムではどういった意味の値を求めようとしているのか答えよ。
情報量の単位
データを覚える最小単位は、0と1の2通りで表される1bit (ビット)と呼ぶ。単位として書く場合には b で表す。さらに、その1bitを8個組み合わせると、256通りの情報を保存できる。256通りあれば一般的な英数字などの記号を1文字保存する入れ物として便利であり、この単位を 1byte (バイト) と呼ぶ。単位として書く場合には B で表す。
通信関係の人は8bit=1byteを1オクテットと呼ぶことも多い。日本語を表現するには、かなや漢字を使うため16bit = 2byte = 1word(ワード) で表現することが多い。(ただしワードは32bitを意味することもあるので要注意, double word=32bit, quad word=64bit という呼び方もある。)
物理では単位が大きくなると、103=kキロ,106=Mメガ,109=Gギガ,1012=Tテラ を使うが、コンピュータの世界では、103≒210=1024 なので、1kB(キロバイト)というと1024Bを意味することが多い。明確に区別する時は、1024B(バイト)=1KiB(キビバイト), 10242B=1MiB(メビバイト), 10243B=1GiB(ギビバイト) などと記載する。
2進数,8進数,16進数
プログラムの中で整数値を覚える場合は、2進数の複数桁で記憶する。例えば、2進数3桁(3bit)であれば、000, 001, 010, 011, 100, 101, 110, 111 で、10進数であれば 0~7 の8通りの値が扱える。(8進数)
2進数4桁(4bit)であれば、0000, 0001, 0010, 0011, 0100, 0101, 0110, 0111, 1000, 1001, 1010, 1011, 1100, 1101, 1110, 1111 の16通りを表現できる(16進数)。これを1桁で表現するために、0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F を使って表現する。
例 8進数 16進数 0123 0x123 ※C言語では、 + 026 + 0xEA 8進数を表す場合、先頭に0をつけて表す。 -------- -------- 16進数を表す場合、先頭に0xをつけて表す。 0151 0x20D 0x3+0xA = 0xD 0x2+0xE = 2+14 = 16 = 0x0 + 桁上がり 0x1+桁上がり = 0x2
整数型と扱える値の範囲
コンピュータの開発が進むにつれ計算の単位となるデータ幅は、8bit, 16bit, 32bit, 64bit と増えていった。整数型データには、正の値しか覚えられない符号無し整数と、2の補数で負の数を覚える符号付き整数に分けられる。
プログラムを作るためのC言語では、それぞれ 8bitの文字型(char)、16bitの short int型、32bitの int 型、64bitの long int 型(※C言語では long int で宣言すると32bitの環境も多いので要注意)があり、それぞれに符号なし整数(unsigned), 符号あり整数(signed: C言語の宣言では書かない)がある。
精度 | 符号あり | 符号なし |
8bit(1byte) | char (int8_t) | unsigned char (uint8_t) |
16bit(2byte) | short int (int16_t) | unsigned short int (uint16_t) |
32bit(4byte) | int (int32_t) | unsigned int (uint32_t) |
64bit(8byte) | long int※ (int64_t) | unsigned long int※ (uint64_t) |
符号付きのデータは、負の数は2の補数によって保存され、2進数の最上位bit(符号ビット)は負の数であれば1、正の数であれば0となる。
整数型で扱える数
(例) 符号なしの1byte(8bit)であれば、いくつの数を扱えるであろうか?
符号なしの N bit の整数であれば2N通りの値を表現でき、0~(2N-1) までの値が扱える。
bit数 | 型 | 符号なし(unsigned) | |
8 | unsigned char | 0~28-1 | 0~255 |
16 | unsigned short int | 0~216-1 | 0~65535 |
32 | unsigned int | 0~232-1 | 0~4294967295 |
符号付きの N bit の整数であれば、2の補数表現では最上位ビットが符号を表すために使われる。
100)10 | 64)16 | 0110,0100)2 |
-100)10 | 9C)16 | 1001,1100)2 |
正の数なら残りの(N-1)bitで扱うため 0〜2N-1-1を表現できる。負の数は2N-1通りを表現できるので、N bit の符号つき整数は、-2N-1 〜0〜 2N-1-1の範囲の値を覚えられる。
bit数 | 型 | 符号あり(signed) | |
8 | char | -27~0~27-1 | -128~127 |
16 | short int | -215~0~215-1 | -32768~32767 |
32 | int | -231~0~231-1 | -2147483648~2147483647 |
2の べき乗 の概算
プログラムを作る場合、2のべき乗がだいたいどの位の値なのか知りたいことが多い。この場合の計算方法として、2つの方法を紹介する。
- 232 = 22 × (210)3 = 4 × 10243 ≒ 4,000,000,000
- 232をN桁10進数で表すとすれば
なので、両辺のlog10を求める。
(つまり、bit数に0.3をかければ10進数の桁数が求まる。)
数値の範囲の問題で動かないプログラム
この話だけだと、扱える数値の上限について実感がわかないかもしれないので、以下のプログラムをみてみよう。(C言語の詳細は説明していないので、問題点がイメージできるだけでいい。)
組み込み系のコンピュータでは、int 型で宣言される変数でも、16bitの場合もある。以下のプログラムは期待した値が計算できない例である。以下の例では、16bit int型として short int で示す。
// ✳️コード1 #include <stdio.h> #include <math.h> int main() { // 原点から座標(x,y)までの距離を求める short int x = 200 ; short int y = 200 ; short int r2 = x*x + y*y ; // (x,y)までの距離の2乗 short int r = sqrt( r2 ) ; // sqrt() 平方根 printf( "%d\n" , r ) ; // 何が求まるか? return 0 ; // (例) 200√2 = 282ではなく、120が表示された。 }
コンピュータで一定時間かかる処理を考えてみる。
// コード2.1 // 1 [msec] かかる処理が以下のように書いてあったとする。 short int i ; for( i = 0 ; i < 1000 ; i++ ) NOP() ; // NOP() = 約1μsecかかる処理とする。 // ✳️コード2.2 // 0.5 [sec]かかる処理を以下のようにかいた。 short int i ; for( i = 0 ; i < 500000 ; i++ ) NOP() ; // でもこの処理は16bitコンピュータでは、1μsecもかからずに終了する。なぜか?
上記の例は、性能の低い16bit コンピュータの問題で、最近は32bit 整数型のコンピュータが普通だし、特に問題ないと思うかもしれない。でも、32bit でも扱える数の範囲で動かなくなるプログラムを示す。
OS(unix) では、1970年1月1日からの経過秒数で時間(unix時間)を扱う。ここで、以下のプログラムは、正しい値が計算できなかった有名な例である。(2004年1月11日にATMが動かなくなるトラブルの原因だった)
// ✳️コード3.1 int t1 = 1554735600 ; // 2019年4月09日,00:00 int t2 = 1555340400 ; // 2019年4月16日,00:00 // この2日の真ん中の日を求める。 // t1 | t2 // |--------+--------| // | t_mid | // 以下のプログラムは、正しい 2019年4月12日12:00 が求まらない。なぜか? int t_mid = (t1 + t2) / 2; // (例) 1951年03月25日 08:45 になった。 // コード3.2 // 以下のプログラムは正しく動く。 time_t 型(時間処理用の64bit整数)を使えば問題ない。 time_t t1 = 1554735600 ; // 2019年4月09日,00:00 time_t t2 = 1555340400 ; // 2019年4月16日,00:00 // time_t型が32bitであったとしても桁溢れない式 time_t t_mid = t1 + (t2 - t1) / 2 ;
練習問題2
以下の整数の範囲を具体的な値で答えよ。
出席番号・自分の誕生日(の日にち)に合わせて該当する2問について答えること。
- 7bitの符号なし整数で扱える数値の範囲 (出席番号が偶数)
- 12bitの符号あり整数で扱える数値の範囲 (出席番号が奇数)
- 20bitの符号なし整数で扱える数値の範囲 (誕生日の日づけが偶数)
- 24bitの符号あり整数で扱える数値の範囲 (誕生日の日づけが奇数)
練習問題3
先に示した数値の範囲が原因で動かないプログラム(コード1,コード2.2,コード3.1)の中から1つを選んで、計算結果が正しく求まらない原因を、具体的な値を示しながら説明せよ。
練習問題1,練習問題2,練習問題3について、レポートとして提出せよ。
Teamsのこちらの共有フォルダに、回答記入用のひな型がおいてあるので、この書式を参考に各自レポートにまとめ、同フォルダに提出してください。
PHPとデータベースによるバックエンドプログラミング
前回の講義では、Webページの作り方として、JavaScriptを用いたブラウザで動くプログラミングについて説明を行った。今回の授業では、データを管理しているサーバ側(バックエンド)で使われるプログラミング言語 PHP についての紹介と、データを管理するためのプログラム言語 SQL について説明し、簡単な演習をレポート課題とする。
PHPとデータベースによるバックエンドプログラミング
- PHPとデータベースによるバックエンドプログラミング (リンク修正済み)
- 以下のサンプル(sampleD.php~) PHP のファイルなので、ダウンロードしたファイルを開いてもこのままでは動きません。動作確認のページにて実行結果を確認してください。
- PHPによるHelloWorld
- PHPによるデータの受け取り
- データベースとは
- sampleG-itemlist.sql
- sampleG-userlist.sql
- sampleG-buylist.sql
- Paiza.io の itemlist,userlist,buylist の動作確認ページ – このページにてSQLの練習問題を答えてください
- PHPの中でSQLを使う
- 05/08 練習問題のレポート提出先はこちら (リンク修正済み)
値渡しと参照渡しとポインター
Javaでの引数に対する副作用
Javaでのプログラムにおいて、下記のように関数に引数でデータが渡された場合、呼び出し元の変数が変化する/変化しないの違いが分かるであろうか?
import java.util.*; class A { private int a ; public A( int x ) { a = x ; } public void set( int x ) { a = x ; } public int get() { return a ; } } public class Main { public static void foo( int x , Integer y , String s , int z[] , A a ) { x = 12345 ; // プリミティブな引数の書き換え y = 23456 ; // イミュータブルな引数の書き換え s = "hoge" ; z[0] = 34567 ; // 参照で渡されたオブジェクトの書き換え a.set( 45678 ) ; } public static void main(String[] args) throws Exception { int mx = 11111 ; // プリミティブなデータ Integer my = 22222 ; // イミュータブルなオブジェクト String ms = "aaa" ; int mz[] = { 33333 } ; // それ以外のオブジェクト A ma = new A( 44444 ) ; foo( mx , my , ms , mz , ma ) ; System.out.println( "mx="+mx+",my="+my+",ms="+ms+",mz[0]="+mz[0]+",ma="+ma.get() ); } }
上記のプログラムでは、foo() の第1引数 mx は、プリミティブ型なので関数の引数に渡される際には、コピーが生成されて渡されるため、呼び出し元の変数 mx の値は変化していない。
Javaでは、プリミティブ型以外のデータは、ヒープ領域に実体が保存され、そのデータの場所(ポインタ)によって管理される。
しかし、Integer型のオブジェクト my や、String型のオブジェクト ms は、参照(データの場所)が渡されるが、イミュータブルな(変更できない)オブジェクトなので、値の代入が発生すると新しいオブジェクトが生成され、そのアドレスが参照を保存している変数(ポインタ)に代入される。このため、呼び出し元の my や ms は値が変化しない。
これに対し、配列 mz や クラスオブジェクト ma は、オブジェクトの中身を関数 foo で値を変更すると、呼び出し元の変数の内容が変更される。こういった関数やメソッドの呼び出しによって、呼び出し元の値が変化することは「副作用」と呼ばれる。
こういった参照のメカニズムは、データの管理の仕方を正しく理解する必要があることから、もっと原始的な C 言語にて理解を目指す。
C言語の基礎
#include <stdio.h> int main() { int n ; scanf( "%d" , &n ) ; // 標準入力から整数をnに保存 int m = 1 ; for( int i = 1 ; i <= n ; i++ ) m *= i ; printf( "%d! = %d\n" , n , m ) ; // return 0 ; }
printf の最初の引数は、表示する際のフォーマットであり、%d の部分には対応する引数の値に置き換えて表示される。
型 | 基数 | 型 | 表示方式 long int %ld | 10進数 %d | double %lf | 固定小数点表示 %f 12.34 int %d | 16進数 %x | float %f | 指数小数点表示 %e 1.234e+1 short int %hd | 8進数 %o | | 固定/指数自動 %g char %c | | printf( "%5.2f" , 1.2345 ) ; □1.23 char[], char* %s | |
// Compile by C++ #include <stdio.h> int main(void) { long int x = 123456789L ; int y = 1234567 ; short int z = 32767 ; printf( "%ld %d %hd\n" , x , y , z ) ; // 123456789 1234567 32767 printf( "%d %x %o\n" , 0x1000 , 32767 , 32767 ) ; // 4096 7fff 77777 double p = 123.45678L ; float q = 12.345 ; printf( "%lf %f\n" , p , q ) ; // 123.456780 12.345000 printf( "(%lf) (%8.3lf) (%le)\n" , p , p , p ) ; // (123.456780) ( 123.457) (1.234568e+02) char c = 0x41 ; char s[] = "ABCDE" ; char t[] = { 0x41 , 0x42 , 0x43 , 0x0 } ; // C言語の文字列の末尾には'// Compile by C++ #include <stdio.h> int main(void) { long int x = 123456789L ; int y = 1234567 ; short int z = 32767 ; printf( "%ld %d %hd\n" , x , y , z ) ; // 123456789 1234567 32767 printf( "%d %x %o\n" , 0x1000 , 32767 , 32767 ) ; // 4096 7fff 77777 double p = 123.45678L ; float q = 12.345 ; printf( "%lf %f\n" , p , q ) ; // 123.456780 12.345000 printf( "(%lf) (%8.3lf) (%le)\n" , p , p , p ) ; // (123.456780) ( 123.457) (1.234568e+02) char c = 0x41 ; char s[] = "ABCDE" ; char t[] = { 0x41 , 0x42 , 0x43 , 0x0 } ; // C言語の文字列の末尾には'\0'が必要 printf( "(%c) (%s) (%s)\n" , c , s , t ) ; // (A) (ABCDE) (ABC) return 0 ; }'が必要 printf( "(%c) (%s) (%s)\n" , c , s , t ) ; // (A) (ABCDE) (ABC) return 0 ; }
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 ; }
ポインタの加算と配列アドレス
ポインタに整数値を加えることは、アクセスする場所が、指定された分だけ後ろにずれることを意味する。
// ポインタ加算の例 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 だけを記述すると、配列の先頭を意味することに注意。
ポインタインクリメントと式
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 ] = '¥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++) } // ポインタ加算と代入と'¥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++) } // ポインタ加算と代入と'¥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++) } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ) *p++ = *q++ ; // *(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++) } // ポインタ加算と代入と'¥0'判定を一度に書く void strcpy( char* p , char* q ) { while( (*p++ = *q++) != '\0' ) // while( *p++ = *q++ ) ; でも良い ; }' ) // while( *p++ = *q++ ) ; でも良い ; }