こんにちは。
プログラミングの勉強のために筑波大の講義「システムプログラミング」をやってみようと思います。
このエントリは完全な個人のメモです。
筑波大の講義ページはこちら。
お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。
正直、講義ページがすでにかなり簡潔にまとまっているのでほぼコピペになってしまう気もしています。
第1回の講義内容は以下。
amistad06-a.hatenablog.com
文字、文字列のデータ表現
コンピュータはデータを2進数でしか表現できないので、文字も2進数のデータで保持される。
どの2進数の値がどの文字に対応するのかを決めたものを文字コードという。
文字コードには以下のようないくつかの種類がある。
・ASCII
・shift-JIS
・EUC-JP
・UTF-8
など
ASCIIコード
UNIXで標準的に使用されてきたのはASCIIコード。
ASCIIコードはローマ字、数字、記号、制御コードを含んでいる。
ASCIIコード表はググったら出てくるのでググってください。
BELなどの制御コードも含まれているのが個人的に面白いところ。
EUC-JP
EUCはExtended UNIX Codeの略。
EUC-JPはUNIXで広く使われている日本語文字コード。
基本的には2バイトで漢字1文字を表すが、3バイトで表される補助漢字もある。
バックスラッシュと¥マーク
環境によってはバックスラッシュが¥マークになることがあるが、同じものとみなしてOK。
C言語における文字と文字列
日本語コードは複雑なので、筑波大学の講義ではASCIIコードのみ取り扱う。
C言語では'(シングルクォーテーション)で囲われたものが文字、"(ダブルクォーテーション)で囲われたものが文字列。
'A'は文字、"A"は文字列。
文字と文字列は異なるので注意。
文字
シングルクォーテーションで囲われたデータはchar型の定数で、ASCIIコードに対応している。
char型の変数に'A'を代入するのと0x41を代入するのは同じ。
また、文字は定数であるため、比較演算や算術演算ができる。
文字列
文字の列なので、文字定数の配列として表される。
文字列の終わりを表すために終端文字(\0)が使われる。
最後に必ず終端文字を入れるため、配列のサイズは文字数+1にになる。
標準入出力
通常、標準入力はキーボードであり、標準出力はディスプレイである。
C言語ではキーボードからの入力を標準入力から受け取ることができ、標準出力への出力はディスプレイにされる。
この他にもエラーを出力するための標準エラー出力がある。
UNIXでは標準入出力をパイプやリダイレクションによって他のプロセスやファイルと繋ぐことで柔軟な操作を可能にしている。
標準エラー出力は、標準出力がファイルなどにつながっている場合に人間がエラーに気づきやすくするために標準出力と分けられている。
標準入出力APIを用いたサンプルプログラム。
講義ページのプログラムの解説をコメントで記入しました。
EOFを送信するためにはCtrl + D。
#include <stdio.h>
int main(void)
{
int c;
// getcharで標準入力からデータを読み込む
// データが終わったら(EOFが返ってきたら)while文を抜ける
while ((c = getchar()) != EOF) {
// getchar()で読み込んだデータを
// そのまま標準出力に出力する
putchar(c);
}
return 0;
}
getchar
標準入力から1byteデータを読み込む。
putchar
標準出力に1byteデータを出力する。
上記と同じことがfgetcとfputcを使っても実現できる。
#include <stdio.h>
int main(void)
{
int c;
// getcharと違ってどこからデータを取得するか指定する
while ((c = fgetc(stdin)) != EOF) {
// putcharと違ってどこにデータを出力するか指定する
fputc(c, stdout);
}
return 0;
}
fgetc
引数で指定されたストリームから1byte読み込む。
fputc
引数で指定されたストリームに1byte書き込む。
#include <stdio.h>
#define LINE_LEN 80
int main(void)
{
// データ格納用バッファを準備
char line_buf[LINE_LEN];
// fgetsで標準入力から(LINE_SIZE - 1)Byte読み込んでバッファに書き込む
// EOFか改行文字を読み込んだらそこで読み込みを停止する
// 読み込まれた改行文字はバッファに格納される
// fgetsがエラー(戻り値がNULL)ならreturn
while (fgets(line_buf, LINE_LEN, stdin) != NULL) {
//バッファの中身+改行を標準出力に出力する
puts(line_buf);
}
// fgetsでは改行をバッファに格納するのと、
// putsでは改行を加えて出力するので改行が2回行われる
return 0;
}
fgets
引数で指定されたストリームから1行読む。
puts
引数で指定された文字列+改行を標準出力に出力する
#include <stdio.h>
#define LINE_LEN 80
int main(void)
{
// データ格納用バッファを準備
char line_buf[LINE_LEN];
// fgetsで標準入力から(LINE_SIZE - 1)Byte読み込んでバッファに書き込む
// EOFか改行文字を読み込んだらそこで読み込みを停止する
// 読み込まれた改行文字はバッファに格納される
while (fgets(line_buf, LINE_LEN, stdin) != NULL) {
// バッファの中身を標準出力に出力する
// 終端文字は出力しない
fputs(line_buf, stdout);
}
return 0;
}
fputs
第1引数のバッファの中身を第2引数のストリームに出力する。
putsは標準出力固定だが、fputsは自分で出力先のストリームを指定する。
文字列操作、文字列操作ライブラリ
man 3 stringで文字列操作の関数が見れる。
#include <stdio.h>
#include <ctype.h>
int main(void)
{
int c;
// 標準入力からEOFが来るまで文字を読み込む
// EOFが来たらreturn
while ((c = getchar()) != EOF) {
// 読み込んだ文字が小文字か判定
if (islower(c)) {
// 小文字だったら大文字に変換
c = toupper(c);
// 読み込んだ文字が大文字か判定
} else if (isupper(c)) {
// 大文字だったら小文字に変換
c = tolower(c);
}
// 変換後の文字を標準出力に出力
putchar(c);
}
return 0;
}
islower
引数の文字が小文字かどうか判定する
isupper
引数の文字が大文字かどうか判定する
toupper
小文字を大文字に変換する
tolower
大文字を小文字に変換する
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define LINE_LEN 80
int main(void)
{
int i, len;
// データ格納用バッファを準備
char line_buf[LINE_LEN];
char *p;
// 標準入力から1行読み込んでline_bufに格納
while (fgets(line_buf, LINE_LEN, stdin) != NULL) {
// 読み込んだ行の文字数を取得
len = strlen(line_buf);
// バッファの先頭ポインタを取得
p = line_buf;
// 文字数分だけ繰り返し
for (i = 0; i < len; i++, p++) {
// バッファの中身の大文字と小文字を変換
if (islower(*p)) {
*p = toupper(*p);
} else if (isupper(*p)) {
*p = tolower(*p);
}
}
// line_bufを標準出力に出力する
fputs(line_buf, stdout);
}
return 0;
}
1文字ずつではなく1行ずつ処理を行うプログラムが上記。
使用している関数はすでに説明済みのものなので割愛。
#include <stdio.h>
#include <string.h>
#include <sys/param.h>
int main(void)
{
int i;
// データ格納用のバッファ
char line_buf[MAXPATHLEN];
char *p, *np;
// 標準入力から1行読み込む
// 読み込みに失敗したらreturn
while (fgets(line_buf, MAXPATHLEN, stdin) != NULL) {
i = 0;
// ポインタをバッファの先頭に設定
p = line_buf;
// バッファの中で'/'が最初に現れる位置のポインタを取得
// 現れなかったらNULLが返ってくるのでその場合は何もしない
while ((np = index(p, '/')) != NULL) {
// '/'を'\0'(終端文字)に置き換える
*np = '\0';
// 標準出力に出力する
printf("%d: %s\n", i++, p);
// ポインタを置き換えた文字'\0'(終端文字)の次に移動する
p = np + 1;
}
// 最後の'/'以降の文字列を出力
printf("%d: %s\n", i, p);
}
return 0;
}
index
第一引数の文字列の中で、最初に第二引数の文字が出てくる場所のポインタを返す
第二引数の文字が含まれない場合、NULLを返す
#include <stdio.h>
#include <string.h>
int main(void)
{
// コピー先のバッファを用意
char buf5[5];
char buf20[20];
// コピー元の文字列を用意
char *s1 = "01234567890";
char *s2 = "abcdefghijklmnopqrstuvwxyz";
int len;
// buf5にs1をbuf5のサイズ分だけコピー
// コピー元の方が大きいので、buf5には終端文字が入らない
strncpy(buf5, s1, sizeof(buf5));
printf("copy to buf5: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf5, strlen(buf5));
// buf20にs1をbuf20のサイズ分だけコピー
// コピー先のバッファの方が大きいので、最後に終端文字が追加される
strncpy(buf20, s1, sizeof(buf20));
printf("copy to buf20: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf20, strlen(buf20));
// buf20にs2をbuf20のサイズ分だけ連結
// buf20に収まりきらないので、C言語の使用上、動作は不定
// コピー先のバッファが充分大き場合は必ず終端文字が付与される
strncat(buf20, s2, sizeof(buf20));
printf("cat to buf20: s2=\"%s\", len-s1s2=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s2, len, buf20, strlen(buf20));
return 0;
}
筑波大の講義ではstrlcpyとstrlcatを使用しているが、Linux環境では使用できないので以下の関数で代替
strncpy(char *dest, const char *src, size_t n)
srcをdestにnバイトコピーする
destがnより大きい場合は最後に終端文字を付与する
destがn以下の場合は終端文字は付与されない
strncpy(char *dest, const char *src)
srcをdestにコピーする
srcのポインタから終端文字に当たるまでコピーされる
destが大きく無いとバッファオーバーフローが起こる
strncat(char *dest, const char *src, size_t n)
srcの先頭からnバイト分の文字列をdestに連結する
srcの最後の終端文字は上書きされ、連結後の新たな文字列の最後に終端文字が付与される
destが充分な大きさでない場合、プログラムの動作はC言語の仕様上、不定
strcat(char *dest, const char *src)
srcをdestに連結する
srcの最後の終端文字は上書きされ、連結後の新たな文字列の最後に終端文字が付与される
destが充分な大きさでない場合、プログラムの動作はC言語の仕様上、不定
#include <stdio.h>
#include <string.h>
int main(void)
{
// コピー先のバッファを用意
char buf5[5];
char buf20[20];
// コピー元の文字列を用意
char *s1 = "01234567890";
char *s2 = "abcdefghijklmnopqrstuvwxyz";
int len;
// buf5にs1をbuf5のサイズ分だけコピー
// コピー先のサイズが充分でない場合、バッファの最後には終端文字を入れて
// 書き込めなかったサイズを返す
len = snprintf(buf5, sizeof(buf5), "%s", s1);
printf("copy to buf5: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf5, strlen(buf5));
// buf20にs1をbuf20のサイズ分だけコピー
len = snprintf(buf20, sizeof(buf20), "%s", s1);
printf("copy to buf20: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf20, strlen(buf20));
// buf20にs1とs2をbuf20のサイズ分だけコピー
// コピー先のサイズが充分でない場合、バッファの最後には終端文字を入れて
// 書き込めなかったサイズを返す
len = snprintf(buf20, sizeof(buf20), "%s%s", s1, s2);
printf("cat to buf20: s2=\"%s\", len-s1s2=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s2, len, buf20, strlen(buf20));
return 0;
}
snprintf(char *str, size_t size, const char *format, ...)
strにフォーマット指定子で指定された形式で出力を生成する。
snprintfはsizeを超える文字数を書き込まない。
また、最後に終端文字を付与する。
sprintfとvsprintf
コピー先のバッファサイズが充分にある前提で動作するので使用者は注意が必要。
snprintfとvsnprintfを代わりに使用するのが良い。
その他、文字列操作関数の紹介は割愛
ポインタ
C言語ではプログラマが動的にメモリ領域の確保と開放を行うことができる。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
// コピー先バッファを準備
char *buf5;
char *buf20;
// コピー元文字列を準備
char *s1 = "01234567890";
char *s2 = "abcdefghijklmnopqrstuvwxyz";
int len;
// 5byteのメモリ領域を取得
// 取得に失敗したらNULLを返してくるので、
// NULLだったらプログラムを終了
buf5 = malloc(5);
if (buf5 == NULL) {
perror("malloc");
exit(1);
}
// 20byteのメモリ領域を取得
// 取得に失敗したらNULLを返してくるので、
// NULLだったらプログラムを終了
buf20 = malloc(20);
if (buf20 == NULL) {
perror("malloc");
exit(1);
}
// buf5にs1を5バイト分コピー
strncpy(buf5, s1, 5);
printf("copy to buf5: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf5, strlen(buf5));
// buf20にs1を20バイト分コピー
strncpy(buf20, s1, 20);
printf("copy to buf20: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf20, strlen(buf20));
// buf20にs2を20バイト分コピー
strncat(buf20, s2, 20);
printf("cat to buf20: s2=\"%s\", len-s1s2=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s2, len, buf20, strlen(buf20));
// 確保したメモリ領域を開放
free(buf5);
free(buf20);
return 0;
}
malloc
メモリ領域を取得する
取得したメモリ領域の先頭ポインタを返す
free
指定されたメモリを開放する
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *buf5;
char *buf20;
char *s1 = "01234567890";
char *s2 = "abcdefghijklmnopqrstuvwxyz";
int len;
// 5byteのメモリ領域を取得
buf5 = malloc(5);
if (buf5 == NULL) {
perror("malloc");
exit(1);
}
// 20byteのメモリ領域を取得
buf20 = malloc(20);
if (buf20 == NULL) {
perror("malloc");
exit(1);
}
// buf20を0で埋める
bzero(buf20, 20); /* fill the buffer with zero */
strncpy(buf5, s1, 5);
printf("copy to buf5: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf5, strlen(buf5));
// 確保した領域を超えて書き込みしてみると
// セグメンテーションフォールトなどが起こる
strncat(buf5, s2, 20);
printf("cat to buf5: s2=\"%s\", len-buf5s2=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s2, len, buf5, strlen(buf5));
printf("buf20: str-in-buf=\"%s\", len-str-in-buf=%ld\n", buf20, strlen(buf20));
// 確保したメモリを開放
free(buf5);
free(buf20);
return 0;
}
上記のプログラムを実行するとセグメンテーションフォールトが起こる。
自分でメモリ領域を確保した場合、その領域を超えてアクセスしてはいけない。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char *buf;
char *s1 = "01234567890";
char *s2 = "abcdefghijklmnopqrstuvwxyz";
int len;
// メモリ領域を5byte分確保
buf = malloc(5);
if (buf == NULL) {
perror("malloc");
exit(1);
}
len = strlcpy(buf, s1, 5);
printf("copy to buf: s1=\"%s\", len-s1=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s1, len, buf, strlen(buf));
// bufを開放する前に別のポインタを代入している
// これをしてしまうと5byte分確保した領域に
// アクセスすることができなくなってしまい、
// メモリリークになる
buf = malloc(20);
if (buf == NULL) {
perror("malloc");
exit(1);
}
len = strlcat(buf, s2, 20);
printf("cat to buf: s2=\"%s\", len-bufs2=%d, str-in-buf=\"%s\", len-str-in-buf=%ld\n",
s2, len, buf, strlen(buf));
free(buf);
return 0;
}
上記のプログラムでは意図的にメモリリークを起こしている。
確保したメモリを解放し忘れると、徐々にメモリを圧迫して様々な問題を引き起こすので注意が必要。
まとめ
第2回の講義内容は以上です。
正直私のメモ見るよりも講義ページを見るのが良いと思います。
非常に簡潔にまとまっていてわかりやすいです。
第3回の講義内容は以下。
amistad06-a.hatenablog.com
おわり。