10月から42Tokyoで学んでいるのですが, そこでやった課題の内容及びコードの公開がOKってことなんで, これから気が向いたら42の課題と自分の解答を記事にしていこうと思います. (ただし, 課題PDF自体は再配布や外部への公開が禁止されているので, 課題内容はあくまで自分が要約したものであることをご了承頂きたい)
今回の課題に対する自分の解答のソースコードは以下のリポジトリにうpしてある.
- まえがき (for 42 students)
- 課題内容
- 実装
- 動作例
- ボーナス課題 (複数fd対応)
- static変数について
- 完走した感想
- 参考にしたサイト
まえがき (for 42 students)
42の学生で get_next_line の課題が終了していない方は今すぐブラウザバックすることを強くおすすめします.
本記事には実装レベルでの説明が多く含まれており, 盛大なネタバレを食らうことになります.
課題内容
渡されたファイルディスクリプタ(以降 fd
と呼ぶ)からデータを読み込み, 関数を呼び出す度に1行ずつ(改行文字は除く)渡されたバッファー変数に格納し, 処理結果(正常=1, 異常=-1 or 終了=0)を返す関数を作成する.
読み込む際には事前に #define
された BUFFER_SIZE
という定数分を一度に読み込む必要がある. read(fd, &buf, BUFFER_SIZE);
プロトタイプ宣言は以下のようにすること.
int get_next_line(int fd, char **line);
課題の意図としては以下の2つがメインみたいです.
- 行ごとに読み込むという汎用性の高い関数を実装する
- C言語のstatic変数について理解する
制約
今回使用可能な標準Cライブラリで定義されている関数は以下の通りです.
- read
- malloc
- free
また, 42にはNorminetteというLinterプログラムがあり, Linterプログラムがエラーを吐いた場合, 課題は問答無用で0点となる.
Norminetteが課す制約はいくつもあり, すべて列挙するのは大変なのだが, 大きな制約としては以下の通りである.
- 各行は最大80文字
- 各関数には最大25行まで
- 各関数内で定義出来る変数は5つまで
- 各関数内で受け取れる名前付きパラメータ4つまで
- 1行につき1命令 (例えば
int a= 0
は宣言と代入を同時に行っているので不可) - 列挙型には
e_
のプレフィックスを付けるなどのいくつかの命名規則 - 以下の命令の使用禁止
- for
- do..while
- switch
- case
- goto
これ以外にも複数の制約があり, もし自分のコード内で 「なんでこんな面倒くさい書き方してるんだ?」って思うことがあったら恐らくそれはNorminetteのエラーを消すためにそのように書いてる場合が多い. (そうじゃないときもあるので遠慮なくコメントください)
また, 本来はNorminette違反ですが, ブログのためにコメントを複数追加しています.
提出ファイル
- get_next_line.c
- get_next_line_utils.c
- get_next_line.h
この3つのファイルのみを提出すること.
実装
get_next_line.h
# include <stdlib.h> # include <unistd.h> # define SUCCESS 1 # define END_OF_FILE 0 # define ERROR -1 # define CONTINUE_READ -2 # ifndef BUFFER_SIZE # define BUFFER_SIZE 256 # endif int get_next_line(int fd, char **line); char *ft_strdup(const char *s); char *ft_substr(char const *s, unsigned int start, size_t len); size_t ft_strlen(const char *s); char *ft_strchr(const char *s, int c); char *ft_strjoin(char const *s1, char const *s2);
基本的には定数の定義と get_next_line_utils.c
の関数のプロトタイプ宣言入っているだけ.
get_next_line.c
get_next_line.c では以下の関数を定義している
int get_next_line(int fd, char **line); static int read_process(int fd, char **line, char **save); static int join_line_and_save(char **line, char **save); static int join_line_and_buf(char **line, char *buf); static int split_by_newline(char **line, char **save, char *buf);
int get_next_line(int fd, char **line);
get_next_line.c 内で定義している他の関数を呼び出して動作する. 読み込んだ文字列でline
に結合しなかった残りの文字列を char*型のstatic変数 save
で次回関数が呼ばれた時のために保管している.
int get_next_line(int fd, char **line) { int ret; static char *save; if (fd < 0 || !line || BUFFER_SIZE <= 0 || !(*line = ft_strdup(""))) return (ERROR); ret = CONTINUE_READ; if (save) ret = join_line_and_save(line, &save); if (ret == CONTINUE_READ) ret = read_process(fd, line, &save); return (ret); }
やっていることとしては
- char*型のstatic変数
save
を宣言 - 以下の条件の時は問答無用でエラーとして返す
fd
が負の値line
がNULLBUFFER_SIZE
が0以下line
の初期化処理に失敗 (!(*line = ft_strdup(""))
の部分)
- もし前回読み込んだ文字列が残っていたら
join_line_and_save(line, &save)
を呼び出して前回読み込んだ残りの文字列をline
に格納する. もしsave
内に改行が入っていた場合,ret
にはSUCCESS
が入る. 改行が入っていない場合はCONTINUE_READ
が入る. - 前回読み込んだ文字列の残りの中に改行がない場合は
fd
から新たに読み込む.
といった感じです.
基本的にはバリデーションと前回読み込んだ文字列の保持などを行っています. 実際にfd
から読み込んだり, 文字列から改行を検出して文字列を分割するのは他の関数の仕事です.
static int join_line_and_save(char **line, char **save);
前回 get_next_line() が呼び出された時に保存した save
を line
に何文字コピーするかを計算し, コピーする.
static int join_line_and_save(char **line, char **save) { char *tmp; char *newline_ptr; if ((newline_ptr = ft_strchr(*save, '\n'))) { tmp = *line; *line = ft_substr(*save, 0, newline_ptr - *save); free(tmp); if (!(*line)) return (ERROR); tmp = *save; *save = ft_substr(newline_ptr + 1, 0, ft_strlen(newline_ptr + 1)); free(tmp); if (!(*save)) return (ERROR); return (SUCCESS); } else { tmp = *line; *line = *save; *save = NULL; free(tmp); return (CONTINUE_READ); } }
やってることとしては
- 前回読み込んだ文字列の残り
save
に改行が入っている場合はline
に改行文字までの文字列をコピーし, 残りの文字列をsave
に代入してSUCCESS
を返す. - 前回読み込んだ文字列の残り
save
に改行が入っていない場合はline
にsave
の文字列を全てコピーする.
static int read_process(int fd, char **line, char **save);
read()
関数を呼び出し, 読み込まれたデータを save
に保存する. 読み込んだ文字列に改行が入っていた場合にはそこまでを line
に入れて, 残りを save
に入れる.
static int read_process(int fd, char **line, char **save) { ssize_t read_size; int ret; char *buf; ret = CONTINUE_READ; if (!(buf = malloc(BUFFER_SIZE + 1))) return (ERROR); while (ret == CONTINUE_READ && (read_size = read(fd, buf, BUFFER_SIZE)) > 0) { buf[read_size] = '\0'; if (ft_strchr(buf, '\n')) ret = split_by_newline(line, save, buf); else ret = join_line_and_buf(line, buf); } free(buf); if (ret == CONTINUE_READ && read_size == 0) ret = END_OF_FILE; else if (ret == CONTINUE_READ && read_size == -1) ret = ERROR; return (ret); }
やっていることとしては
ret == CONTINUE_READ
だったらfd
からBUFFER_SIZE
分文字を読み込みbuf
に入れる.- もし
buf
の中に改行文字が入っていたらsplit_by_newline()
を呼び出して改行文字までをline
と結合し, 残りをsave
に入れてループを抜ける. (ret == SUCCESS
になるから) - もし
buf
の中に改行文字が入っていなかったらjoin_line_and_buf()
を呼び出して単純にline
とbuf
を結合する. - ループ処理終了後, もし
ret == CONTINUE_READ && read_size == 0
だったら, これすなわち改行は見つからずにfd
がEOFに達したということを意味するのでEND_OF_FILE
を返す. read_size == -1
ということは読み込みに失敗しているのでERROR
を返す.- 上記2つのif文の条件を通過したということは正常に終了したかEOFに到達したということなのでそのまま
ret
を返す.
static int split_by_newline(char **line, char **save, char *buf);
buf
内で最初に出現した改行文字より前を line
に, それより後ろを save
に格納する.
static int split_by_newline(char **line, char **save, char *buf) { char *old_line; char *tmp; char *newline_ptr; newline_ptr = ft_strchr(buf, '\n'); if (!(tmp = ft_substr(buf, 0, newline_ptr - buf))) return (ERROR); old_line = *line; *line = ft_strjoin(*line, tmp); free(old_line); free(tmp); if (!(*line)) return (ERROR); if (!(*save = ft_substr(newline_ptr + 1, 0, ft_strlen(newline_ptr + 1)))) return (ERROR); return (SUCCESS); }
やっていることとしてはこの関数の説明をそのままコードにしただけなので詳細な説明は省く.
static int join_line_and_buf(char **line, char *buf);
line
と buf
を単純に結合するだけ.
static int join_line_and_buf(char **line, char *buf) { char *tmp; tmp = *line; *line = ft_strjoin(*line, buf); free(tmp); if (!(*line)) return (ERROR); return (CONTINUE_READ); }
やっていることとしてはこの関数の説明をそのままコードにしただけなので詳細な説明は省く.
get_next_line_utils.c
以下の関数が定義されている.
char *ft_strdup(const char *s); char *ft_substr(char const *s, unsigned int start, size_t len); size_t ft_strlen(const char *s); char *ft_strchr(const char *s, int c); char *ft_strjoin(char const *s1, char const *s2);
基本的には ft_
というプレフィックスを取った標準ライブラリ関数と同じ動きをする. (例: ft_strdup()
-> strdup()
)
これらに関しては標準ライブラリ関数とほぼ同じなので実装レベルで説明は行わない.
詳細を知りたい場合は以下のリンクから見ることが出来る.
動作例
実際の動きを見ていく.
以下のような main.c
を用意する.
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include "get_next_line.h" int main(int argc, char *argv[]) { char *file_name; if (argc == 1) { file_name = "test.txt"; } else file_name = argv[1]; int fd = open(file_name, O_RDONLY); // int fd = 999999; char *line; int status; printf("\n[START] %s\n\tfd: %d\n\n", file_name, fd); do{ status = get_next_line(fd, &line); printf("status: \t%d\n", status); printf("read txt:\t%s\n", line); free(line); }while (status == 1); printf("\n[FINISH] %s\n\tstatus: %d\n\n", file_name, status); close(fd); }
そしてこれをコンパイルする gcc -D BUFFER_SIZE=256 main.c get_next_line.c get_next_line_utils.c
./a.out
で実行するとtest.txt
から1行ずつ読み込めていることがわかるだろう.
ちなみに./a.out
の引数としてファイル名を指定することもでき, ./a.out main.c
とすると main.c
自身が1行ずつ読み込まれて出力される.
[START] main.c fd: 3 status: 1 read txt: #include <stdio.h> status: 1 read txt: #include <sys/types.h> status: 1 read txt: #include <sys/stat.h> status: 1 read txt: #include <fcntl.h> status: 1 read txt: #include "get_next_line.h" status: 1 read txt: status: 1 read txt: int main(int argc, char *argv[]) status: 1 read txt: { status: 1 read txt: char *file_name; status: 1 <--------------------- 長いので中略 -------------------------------> read txt: }while (status == 1); status: 1 read txt: printf("\n[FINISH] %s\n\tstatus: %d\n\n", file_name, status); status: 1 read txt: status: 1 read txt: close(fd); status: 1 read txt: } status: 0 read txt: [FINISH] main.c status: 0
BUFFER_SIZE による実行時間とシステムコール呼び出し回数の変化
また, コンパイル時に指定していた定数BUFFER_SIZE
を変更することで一度のread()
で読み込む量が変わる.
実際にBUFFER_SIZE
の値を変更するとどのくらい変わるのか, システムコールが呼び出された回数が見れるコマンド strace
を使って計測してみた.
$ gcc -D BUFFER_SIZE=1 main.c get_next_line.c get_next_line_utils.c && strace -c -e trace=read ./a.out main.c [START] main.c <--------------------- 長いので中略 -------------------------------> [FINISH] main.c status: 0 % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.001448 2 634 read ------ ----------- ----------- --------- --------- ---------------- 100.00 0.001448 634 total
BUFFER_SIZE=1
の時, システムコールread() が 634回呼ばれています.
BUFFER_SIZE=10
に設定して同様のコマンドでコンパイルして実行すると
$ gcc -D BUFFER_SIZE=10 main.c get_next_line.c get_next_line_utils.c && strace -c -e trace=read ./a.out main.c [START] main.c <--------------------- 長いので中略 -------------------------------> [FINISH] main.c status: 0 % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 66 read ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 66 total
BUFFER_SIZE=100
にすると以下のようになります.
$ gcc -D BUFFER_SIZE=100 main.c get_next_line.c get_next_line_utils.c && strace -c -e trace=read ./a.out main.c [START] main.c <--------------------- 長いので中略 -------------------------------> [FINISH] main.c status: 0 % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000038 4 9 read ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000038 9 total
BUFFER_SIZE
を大きくするとシステムコール read()
が呼ばれる回数が少なくなっているのがわかりますね!
試しにめっちゃでかいファイルを作って, それの読み込み速度を比較してみましょう.
$ for i in `seq 1 1000000` # めっちゃでかいファイルを作る do echo "${i}" >> big_file.txt done $ $ ls -lh big_file.txt # ファイルサイズは6.6MB -rw-rw-r-- 1 jun jun 6.6M 12月 18 06:45 big_file.txt $ $ wc -m big_file.txt # 文字数は6888896文字 6888896 big_file.txt
BUFFER_SIZE
を変えてテストしてみましょう.
なお, このテストでは main.c
内にあるwhileループ内のprintf
はコメントアウトして実行するようにしています.
テストは以下のような形で行い, total の時間を見ます. ついでに read() が呼ばれた回数も見ます.
$ gcc -D BUFFER_SIZE=100 main.c get_next_line.c get_next_line_utils.c && time ./a.out big_file.txt && strace -c -e trace=read ./a.out big_file.txt [START] big_file.txt fd: 3 [FINISH] big_file.txt status: 0 ./a.out big_file.txt 1.03s user 0.02s system 99% cpu 1.050 total [START] big_file.txt fd: 3 [FINISH] big_file.txt status: 0 % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.058994 0 68891 read ------ ----------- ----------- --------- --------- ---------------- 100.00 0.058994 68891 total
BUFFER_SIZE=1
の時は 5.56秒でした.
このような感じで何回かBUFFER_SIZE
を変更して実行時間の変化を見ます.
BUFFER_SIZE | 実行時間[s] | read()が呼ばれた回数 |
---|---|---|
1 | 5.6 | 6888898 |
10 | 0.742 | 688892 |
100 | 1.051 | 68891 |
1000 | 9.4 | 6891 |
10000 | 101 | 691 |
うーむ, BUFFER_SIZE=10
が一番速いですね...
イメージとしてはBUFFER_SIZE
が大きくなればなるほど早くなると思ったのですが...
プロファイルツールを用いたボトルネックの特定
gcc
付属のプロファイルツール gprof
を使ってどの関数がどれくらい呼ばれて, どのくらい時間がかかったかを計測してみました.
$ gcc -pg -D BUFFER_SIZE=1000 main.c get_next_line.c get_next_line_utils.c && ./a.out big_file.txt && gprof ./a.out % cumulative self self total time seconds seconds calls us/call us/call name 78.50 7.14 7.14 6012695 1.19 1.19 ft_strlen 21.31 9.08 1.94 2000000 0.97 3.34 ft_substr 0.33 9.11 0.03 1013778 0.03 0.03 ft_strchr 0.22 9.13 0.02 1000001 0.02 9.14 get_next_line 0.11 9.14 0.01 1000000 0.01 7.86 join_line_and_save 0.00 9.14 0.00 1000001 0.00 1.19 ft_strdup 0.00 9.14 0.00 6890 0.00 10.31 read_process 0.00 9.14 0.00 6889 0.00 2.38 ft_strjoin 0.00 9.14 0.00 6889 0.00 10.28 split_by_newline
おおぉ... 自分としてはてっきりメモリコピー系が時間かかっているかと思ったのですが, まさかのft_strlenが一番のボトルネックだったとは...
ft_strlen()
の実行回数が多いのは, get_next_line_utils.c
内で大量に ft_strlen()
呼んでいるからでしょうな...
ちょっとgrepして, ft_strlen()
を使用している場所を抜き出してみた
get_next_line.c:28: *save = ft_substr(newline_ptr + 1, 0, ft_strlen(newline_ptr + 1)); get_next_line.c:60: ft_strlen(newline_ptr + 1)))) get_next_line_utils.c:31: s_len = ft_strlen(s); get_next_line_utils.c:53: if (start >= ft_strlen(s)) get_next_line_utils.c:56: s_len = ft_strlen(s); get_next_line_utils.c:92: total_len = ft_strlen(s1) + ft_strlen(s2);
なるほど, 意外と呼び出していないように見えますね... しかし, 多分呼び出し元の関数がかなりの回数呼ばれていて, それが累積してこの結果になったんだと思います.
ここでスパッと解決出来ればかっこいいのですが, ちょっと解決策が思い浮かばないので高速化の方法がある人はぜひ教えてください.
(2020/12/22 追記) ft_strlenを標準ライブラリ実装に置き換えてみる
先程ft_strlen()
の実行時間がプログラム全体の実行時間の中で大きな割合を示すことを調査しました.
じゃあ標準Cライブラリのstrlen()
使ったコードに置き換えたらどうなるか調べてみました.
BUFFER_SIZE | 実行時間[s](ft_strlen) | 実行時間[s](strlen) |
---|---|---|
1000 | 9.4 | 2.6 |
10000 | 101 | 18.9 |
めっちゃ速くなった!!
BUFFER_SIZE=1000
の時の実行時間内訳ではft_strlen()
が7秒かかっていたのですが, 標準実装に変えたらプログラム全体の実行時間が7秒減ったことを考えるとft_strlen()
でかかっていた7秒がまるまる速くなった感じですね.
ちなみにgprof
を使って実行時間の内訳を見ると...
$ gcc -pg -D BUFFER_SIZE=1000 main.c get_next_line.c get_next_line_utils.c && ./a.out big_file.txt && gprof ./a.out % cumulative self self total time seconds seconds calls us/call us/call name 98.70 1.68 1.68 2000000 0.84 0.84 ft_substr 1.18 1.70 0.02 1000000 0.02 1.69 join_line_and_save 0.59 1.71 0.01 main 0.00 1.71 0.00 1013778 0.00 0.00 ft_strchr 0.00 1.71 0.00 1000001 0.00 0.00 ft_strdup 0.00 1.71 0.00 1000001 0.00 1.70 get_next_line 0.00 1.71 0.00 6890 0.00 1.68 read_process 0.00 1.71 0.00 6889 0.00 0.00 ft_strjoin 0.00 1.71 0.00 6889 0.00 1.68 split_by_newline
ft_strlen()
が7秒かかっていたのが消えて, 一番時間がかかった関数がft_substr()
になってますね.
ちなみに自分の ft_strlen()
は以下のようになっており, 特別遅いことをやっているわけでは無いと思うので, 標準Cライブラリの実装が恐ろしく速いのだと思います.
size_t ft_strlen(const char *s) { size_t ans; ans = 0; while (s[ans]) ans++; return (ans); }
ちなみに標準Cライブラリ(glibc)の実装は以下のリンクに載っています. 自分は読んでも何をしているかわからなかったので, わかったらまた記事にしたいと思います.
ボーナス課題 (複数fd対応)
42の課題にはボーナスというものがあります. これは今までの必須パートをクリアした人で余裕があれば挑戦してね〜って感じの内容です.
今回の課題のボーナスは複数fd対応です.
どういうことかというと, fd=3
, fd=4
, fd=5
を順番にget_next_line()
に渡して呼び出した際に, 今の実装だったら save
が1つしかないので順番に呼び出すと他のfd
がsave
を上書きしてしまって上手くいかないので, なんとかして動くようにしましょう! という課題です.
実装
今回は以下の線形リストを表す構造体を定義し, これをうまく使って実装しました.
typedef struct s_list { int fd; char *save; struct s_list *next; } t_list;
基本的に実装は必須パートと同じなのですが, get_next_line()
を結構変更したのと, 線形リスト用の関数(create_fd_elem()
)を作成したのでその2つだけ説明します.
t_list *create_fd_elem(t_list **lst, int fd);
t_list *create_fd_elem(t_list **lst, int fd) { t_list *new; if (!lst) return (NULL); if (!(new = malloc(sizeof(t_list)))) return (NULL); new->fd = fd; new->save = NULL; if (!(*lst)) { *lst = new; new->next = NULL; } else { new->next = *lst; *lst = new; } return (new); }
create_fd_elem()
は新しいfd
が来た時に構造体を動的に確保し, それを初期化してlst
の先頭要素にして, 作成した要素を返す関数です.
int get_next_line(int fd, char **line)
int get_next_line(int fd, char **line) { int ret; static t_list *save_list_head; t_list *target_save_list; if (fd < 0 || !line || BUFFER_SIZE <= 0 || !(*line = ft_substr("", 0, 0))) return (ERROR); target_save_list = save_list_head; // fd のリスト要素を探す while (target_save_list && target_save_list->fd != fd) target_save_list = target_save_list->next; // 見つからなかった場合はリスト要素を作成 if (!target_save_list) if (!(target_save_list = create_fd_elem(&save_list_head, fd))) return (ERROR); ret = CONTINUE_READ; if (target_save_list->save) ret = join_line_from_save(line, &target_save_list->save); if (ret == CONTINUE_READ) ret = read_process(fd, line, &target_save_list->save); if (ret == END_OF_FILE || ret == ERROR) { free(target_save_list->save); // 今回のfdが先頭要素だった場合, 先頭要素を次の要素に変更 if (save_list_head == target_save_list) save_list_head = target_save_list->next; else { // 要素を削除した上でその要素の前後の要素を接続する t_list *tmp = save_list_head; while (tmp->next != target_save_list) tmp = tmp->next; tmp->next = target_save_list->next; } free(target_save_list); } return (ret); }
注意: if (ret == END_OF_FILE || ret == ERROR)
直下の else文 は提出時ありませんでした. なので提出後に修正したものなのでこのままだとNorminetteの行数制限に引っかかります.
今回は行数が多く, 説明が少しむずかしかったのでコメントという形で説明しました.
動作例
複数ファイルを並行して読み込むようなmain.c
を作成します.
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include "get_next_line_bonus.h" int main() { char *line; int fd1 = open("./get_next_line.h", O_RDONLY); int fd2 = open("./get_next_line.c", O_RDONLY); int fd3 = open("./get_next_line_utils.c", O_RDONLY); int status; printf("fd1: %d\n", fd1); printf("fd2: %d\n", fd2); printf("fd3: %d\n", fd3); int finish_count = 0; int idx = 0; while (finish_count < 3) { if (idx % 3 == 0) { printf("\n---------- fd1: %d ----------\n", fd1); status = get_next_line(fd1, &line); printf("status: \t%d\n", status); printf("read txt:\t%s\n", line); free(line); if (status == 1) finish_count = 0; else finish_count++; } else if(idx % 3 == 1) { printf("\n---------- fd2: %d ----------\n", fd2); status = get_next_line(fd2, &line); printf("status: \t%d\n", status); printf("read txt:\t%s\n", line); free(line); if (status == 1) finish_count = 0; else finish_count++; } else if (idx % 3 == 2) { printf("\n---------- fd3: %d ----------\n", fd3); status = get_next_line(fd3, &line) ; printf("status: \t%d\n", status); printf("read txt:\t%s\n", line); free(line); if (status == 1) finish_count = 0; else finish_count++; } idx++; } close(fd1); close(fd2); close(fd3); return 0; }
そしてこの main.c
をコンパイルして実行すると複数ファイル並列で行ごとに読み込めていることがわかると思います.
$ gcc -D BUFFER_SIZE=1000 main_multiple_fd.c get_next_line_bonus.c get_next_line_utils_bonus.c && ./a.out fd1: 3 fd2: 4 fd3: 5 ---------- fd1: 3 ---------- status: 1 read txt: #ifndef GET_NEXT_LINE_H ---------- fd2: 4 ---------- status: 1 read txt: #include "get_next_line.h" ---------- fd3: 5 ---------- status: 1 read txt: #include "get_next_line.h" ---------- fd1: 3 ---------- status: 1 read txt: # define GET_NEXT_LINE_H ---------- fd2: 4 ---------- status: 1 read txt: ---------- fd3: 5 ---------- status: 1 read txt: ---------- fd1: 3 ---------- status: 1 read txt: ---------- fd2: 4 ---------- status: 1 read txt: static int join_line_and_save(char **line, char **save) ---------- fd3: 5 ---------- status: 1 read txt: size_t ft_strlen(const char *s) ---------- fd1: 3 ---------- status: 1 read txt: # include <stdlib.h> <-----------長いので中略-----------> ---------- fd1: 3 ---------- status: 0 read txt: ---------- fd2: 4 ---------- status: 1 read txt: return (ret); ---------- fd3: 5 ---------- status: 0 read txt: ---------- fd1: 3 ---------- status: 0 read txt: ---------- fd2: 4 ---------- status: 1 read txt: } ---------- fd3: 5 ---------- status: 0 read txt: ---------- fd1: 3 ---------- status: 0 read txt: ---------- fd2: 4 ---------- status: 0 read txt: status: 0
static変数について
今回の課題の意図の1つとして static変数 を理解するというのがあるので, 自分なりに調べたり試したりした結果も書いておく. なお, ここでいう static変数 とは関数内のものを指す点に留意して頂きたい.
- static変数はグローバル変数と同じメモリの静的領域に配置される.
- static変数は宣言されたと同時に初期化する必要がある. 例:
static a = 10
- 明示的に初期化しない場合は0で初期化される.
以下のサイトがメモリ領域についてわかりやすかったです.
あとWikipediaの以下の説明もわかりやすいです.
ローカル変数宣言にstaticキーワード(静的記憶クラス指定子)を付加すると、「静的ローカル変数」[3]となり、変数寿命はプログラムの生存期間と同一となる。C言語では静的ローカル変数はグローバル変数と同じくプログラム開始処理以前に一度だけ初期化される
完走した感想
完走した感想(激うまギャグ) ですが, 我ながらうまく書けたと思います.
特にボーナスパートに関しては, 42Tokyo内でもリストを使った実装をした人は少なかったので, ドヤッ! って感じです. ちなみに点数は線形リストの後始末にミスがあり 113/115 点でした (必須パート100点 + ボーナス15点 計算)
自分は基本的に何かしら実装をしたり, 何か開発したりした際にはブログにこうやってアウトプットしたいマンなのですが, 当初42Tokyo内では課題についてどこまで公開してよいか明確な基準は無く, 記事を書くかためらっていました. しかし, 1ヶ月ほど前に公式からルールを守ればOKというのを得たので遠慮なくコードの解説を書きまくりました.
しかし, 書いた本人が言うのもなんですが, 42の課題は自分で考えて調べてやり方を試して実装して, そしてクリア! というのが一番楽しい42の課題の遊び方だと思います. なので申し訳程度にまえがきで注意喚起をしました.
本記事に関してですが, ここまで書くのに5時間ほどかかりました. もともとこんなにかかる予定では無かったのですが, BUFFER_SIZE による時間とシステムコールの呼び出し回数, プロファイラによるパフォーマンス解析などに手を出したらこんなに時間がかかりました.
あとは単純に最近ブログでこういう技術系の記事を書くことが少なかったので, 綺麗な文章を一発で書けなくなってるんですよね. まぁ, アウトプット量を増やせば自然にある程度早く記事が書けるようになるので, これからもいっぱいアウトプットしていきます. ただ, このレベルの文量を毎回書くのはしんどい(特にコードの解説とかインデントの修正とか)ので次からは多分文量が減ると思います.
42ではこのような課題が沢山あり, 人と一緒に相談したり楽しみながらコンピューターサイエンスを手を動かして学べる環境が整っているので自分としては最高に楽しいです. (この課題がまだLv1というのが最高にワクワクしますね)
42は名前だけは調べれば出てくるけど, 中で何をやっているかは正直謎に包まれている部分が多いと思います(課題の情報をアウトプットする人も多くはないですし). なので, 自分の記事を呼んで「へ〜42ってこういう感じなんだな」というのを感じて貰えれば良いなと思います.
42に興味があればとりあえず42の入学試験である Piscine だけでも受けて見ると良いと思います.
参考にしたサイト
- https://www.cyberciti.biz/faq/howto-use-linux-truss-strace-command/
- Summarizing system call activity on Linux systems -- Prefetch Technologies
- strace(1): trace system calls/signals - Linux man page
- プロファイラの比較(+簡単な使い方) - Qiita
- gprofを使いこなす - minus9d's diary
- うさぎでもわかる計算機システム Part13 4つのメモリ領域・システムコール - 工業大学生ももやまのうさぎ塾
- ローカル変数 - Wikipedia