[課題振り返り] libft編
はじめに
42Tokyoのコモンコアを突破したので全ての課題を振り返っていきます。
今回は1番最初の課題であるlibft編です。
課題概要
C言語の標準ライブラリ関数を再実装しそれらを静的ライブラリとしてまとめる課題です。
C言語の基本的な文法やポインタの操作、メモリの確保・開放について学べます。
また、42TokyoのC言語の課題にはコードのフォーマットのようなものがある程度決められており、コーディング初心者でも最低限綺麗なコードが書けるようになっています。
学んだこと
今回はそんなlibftで身についたことや山場となったポイントをまとめていきます。
マニュアルを読む
42Tokyoではこの先の課題でもひたすら課題文や公式ドキュメントを読み込んでいくことになるのですが、それに抵抗がなくなったのは実はこの課題でマニュアルをひたすら読んだからではないかと思います。
C言語の標準ライブラリ関数はterminalでコマンドを打つことでそのマニュアルを確認することができます。以下に例を示します。
$ man isalpha
ISALPHA(3) Library Functions Manual ISALPHA(3)
NAME
isalpha – alphabetic character test
LIBRARY
Standard C Library (libc, -lc)
SYNOPSIS
#include <ctype.h>
int
isalpha(int c);
DESCRIPTION
The isalpha() function tests for any character for which isupper(3) or islower(3)
is true. The value of the argument must be representable as an unsigned char or
the value of EOF.
In the ASCII character set, this includes the following characters (preceded by
their numeric values, in octal):
101 ``A'' 102 ``B'' 103 ``C'' 104 ``D'' 105 ``E''
106 ``F'' 107 ``G'' 110 ``H'' 111 ``I'' 112 ``J''
113 ``K'' 114 ``L'' 115 ``M'' 116 ``N'' 117 ``O''
120 ``P'' 121 ``Q'' 122 ``R'' 123 ``S'' 124 ``T''
125 ``U'' 126 ``V'' 127 ``W'' 130 ``X'' 131 ``Y''
132 ``Z'' 141 ``a'' 142 ``b'' 143 ``c'' 144 ``d''
145 ``e'' 146 ``f'' 147 ``g'' 150 ``h'' 151 ``i''
152 ``j'' 153 ``k'' 154 ``l'' 155 ``m'' 156 ``n''
157 ``o'' 160 ``p'' 161 ``q'' 162 ``r'' 163 ``s''
164 ``t'' 165 ``u'' 166 ``v'' 167 ``w'' 170 ``x''
171 ``y'' 172 ``z''
(抜粋)
これはisalphaという関数のマニュアルです。英語で書かれていることもあり、プログラミング経験の浅い人であれば少し気が引けるような内容かもしれませんが、「関数の再実装」となると欲しい情報が基本的に全て書いてあります。
・その関数を使用するためのヘッダ
・関数プロトタイプ
・挙動の説明
...
抜粋した部分だけでもこれだけの情報が分かり、これは再実装には重要な情報です。
もちろんマニュアルなのでソースとしての信頼性も担保されています。
実際にテストをして調べる
マニュアルを読むことが大事なのはもちろんなのですが、例外的な引数を与えると関数がどんな挙動をするかはマニュアルには書いていないことが多いです。なので自分で実際に本物の標準ライブラリ関数で実験をしてどのような挙動をするのか調べることが必要になります。
これは主に2つの利点があり
・その関数の内部実装が予想できる
・コードの例外を考える力がつく
特にコードの例外を考える力がつくと自分のコードに対するテストをより細かく行うことができるようになります。
型の扱いと環境による違い
C言語は変数の宣言に必ず型が必要でありとても重要な要素になります。
元々int, long, charくらいしか知らなかった型について他の型種類を知ることも出来たのですが、それよりも普段使う型がどんな型なのかを知れる機会になりました。
型はかなり深いのでいくつかのセクションに分けて紹介します。
型ごとのサイズ(バイト数)
環境に依存するのですが例えば筆者の環境で代表的なものをまとめます。
・char -> 1バイト
・int -> 4バイト
・long long -> 8バイト
・size_t -> 8バイト
・ポインタ(型によらない) -> 8バイト
繰り返しますがこれは環境に依存するので、大事なのは型ごとのサイズを知っておくこと自体ではありません。これを通じてその型がどんな役割を持つのか知ることです。(Cの規格を合わせて読むとより良い)
・char -> 文字に使われる(asciiの表現には十分)
・int -> 整数に使われる
・long long -> intより大きい整数に使われる
・size_t -> 配列のindexなどに使われる(その用途から環境の中では最も大きな非負整数型であることが多い)
・ポインタ -> アドレスに使われる(なので型によらない)
値の範囲について
たとえばint型の値の範囲をご存知でしょうか?
少し勉強した人であれば"2^31-1"と答えると思いますがこれは語弊を生みやすいです。
2^31-1という答えは恐らく「int型は32bitの符号付き整数型なので、符号分を抜いてこうなる」という考えだと思います。
ですが、C99の規格には以下のように書いてあります。
「limits.hで定義されたINT_MINからINT_MAXの範囲の値を含むのに十分大きい型」
そしてINT_MAXは規格では「2^15-1を最小値として実装ごとに置き換えられる」と書かれています。
これが結果的にint型が32bitの環境が多いので、一般的には2^31-1が最大値となると言われています。
規格通りならint型は16bitにも、64bitにもなりうるのでその最大値を決まった値で表現することは語弊を生みます。
こういった環境による背景を知っていることで自然と以下のようなコードを書きます。
分かりやすい例でいえばint型を超えているかどうかの条件式を
if (value > 2147483647)
と書くとint型が32bitの環境を前提としている(それが悪いこととは言い切れないが規格を読んでいればこうはならない)ので、
if (value > INT_MAX)
のように書きます。(可読性の観点だけじゃない)
配列のインデックスが何をしているのか
#include <stdio.h>
int main(void)
{
char str[] = "Hello";
printf("%c\n", str[2]);
return (0);
}
(実行結果)
$ cc test.c
$ ./a.out
l
上記はstrのインデックス2(3文字目)を出力するコードです。
慣れてしまうと考えることはないであろうこのインデックスが何をしているのか学べます。
配列のインデックスはポインタ演算をしているだけです。
strというのがその配列の先頭の要素のアドレスを示していて、[i]は型のサイズを足し算していると言い換えることができます。
#include <stdio.h>
int main(void)
{
char str[] = "HelLo";
printf("%c\n", str[2]);
printf("%c\n", (str + 2 * sizeof(char))[0]);
return (0);
}
$ cc test.c
$ ./a.out
l
l
なので、上記のようにアドレスの足し算をして[0]にアクセスしても同じ結果が得られます。
メモリ確保
strdupなどで急にmallocが使用可能関数に入ってきます。
それまでのmallocの理解は「動的メモリの確保ができる」くらいで普通の配列として宣言した時との違いもよくわかっていませんでした。
C言語には静的メモリと動的メモリがあり各関数は処理される時にstack形式で呼び出し元の関数の上に静的メモリとして確保され、その関数を抜けるとメモリがなくなるので例えば以下のようなコードは期待しない挙動をする恐れがあります。
#include <stdio.h>
char* generate_greeting(void)
{
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
return (greeting);
}
int main(void)
{
char *greeting = generate_greeting();
printf("%s\n", greeting);
return (0);
}
(実行結果)
$ cc test.c
test.c:6:13: warning: address of stack memory associated with local variable 'greeting' returned [-Wreturn-stack-address]
return (greeting);
^~~~~~~~
1 warning generated.
$ ./a.out
xS
筆者の環境ではwarningが出てくれていますが、実行は出来てしまうのでこのようなことが起こり得ます。
このように関数を跨ぐ時は以下のように動的メモリを確保する必要があります。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* generate_greeting(void)
{
char *greeting = malloc(sizeof(char) * 6);
if (greeting == NULL)
return (NULL);
strcpy(greeting, "Hello");
return (greeting);
}
int main(void)
{
char *greeting = generate_greeting();
printf("%s\n", greeting);
free(greeting);
return (0);
}
(実行結果)
$ cc test.c
$ ./a.out
Hello
また、メモリ確保にはメモリ開放がつきまといます。
42ではmemory leakが許されないのでmallocしたメモリが使用し終えたら必ずfreeしてあげる必要があります。libftではこれが単純ですが、後々の課題で重要な要素になります。
リスト構造
libftでは標準ライブラリ関数とは別にリストを操作する関数を作成します。リスト構造とは以下のように次の要素のポインタを持った構造体です。
typedef struct s_list
{
void *content;
struct s_list *next_ptr;
} t_list;
リスト構造を学ぶことで、構造体のみではなくC言語では一生付き合っていく重要な要素であるポインタの概念をより深く理解することができます。
普通の配列との違い(アドレスが連続していない)やそれによる拡張性などを学ぶことができます。
静的・動的ライブラリ
libftは静的ライブラリを生成する課題ですが、静的があるのなら動的もあるのかと興味で調べました。
簡単に違いを説明するとリンク時に、静的ライブラリは関数の組み込みを、動的ライブラリは関数の呼び出し情報の組み込みをします。
ライブラリを自身で作成することは今後の課題で可読性の向上などに繋がります。
(動的ライブラリは共有ライブラリとも言います。)
Makefile
Makefileの書き方も学びます。別にMakefileがC言語の機能だとかそういうわけではないですが、C言語との相性がとても良いです。
・C言語はファイルごとにコンパイルされそれらを元に実行ファイルが作成される
->変更のあったファイルのみを再コンパイルしてくれる。
・C言語は実行ファイルの作成と実行の手順が多い
->1つのコマンドで全て実行してくれる。
・移植性
->一度書いてしまえば、少し書き換えるだけで他のプロジェクトに使いまわせる。
review
これはこの課題のというよりは42の良いところなのですが、最初の課題の振り返りということで話しておきます。42ではreviewする側もされる側も何度も経験することになるのですが、やはりreviewには多くの利点があります。
・初心者のうちから可読性を意識するようになる
・コードについての議論を通して理解や思考が深くなる
・自分のコードに責任を持てる
など他にもいろいろ挙げられると思います。
そもそもreviewは会社に入ってしまえば日常的に行われるのに学生のうちから経験できる場所はかなり限られていますし、単純な回数でいえば42よりも多く経験できる場所は無いと思います。(主観ですが)
Discussion