この記事はN高グループ・N中等部 Advent Calendar 2024、6日目の記事です。
はじめに
こんにちは。とあるプロクラTAです。今年の春に大阪から上京して、42Tokyoというエンジニアの学校で、基礎からプログラミングを学びながら、角川ドワンゴ学園通学プログラミングコースでプログラミング講師としてインターンをしています。
主に今まで、メインでやってきた分野はゲーム分野で、その他にも、競プロ、C、Web系も少しだけ触ったことがあります。
そもそもC言語ってどんな言語なの?
C言語は高水準言語の特徴を持ちながら、メモリ管理など、ハードウェア寄りの記述も可能な低水準言語の特徴も併せ持つ、現在も人気の高い言語の中では最も古くからある、プログラミング言語です。
自由度と拡張性が高く、処理速度も速いので私は、C言語はただの神ゲー。だと思っています。
2024年冬現在、私が通っている42Tokyoも、主に学習に使う言語はC言語です。私自身、42の受験をきっかけにCの学習を始めました。
早速C言語を書いてみよう!
難しそうな計算をしたり、文字列を操作してみたりしたい!と思った方々も複数名いらっしゃるかと思いますが、まずは出力ができないとお話にならないということで、初回である、今回はwriteとprintfという、二つの基本的な出力方法について、お話ししたいと思います。(まあ、2回目があるかはわかりませんが。)
write
まずはwriteから見ていきましょう。writeはもっとも基礎となる出力のための関数です。実際に直接コンピュータに書き込みを指示しているのがこの関数であるため、後述のprintfなど他の出力系関数はwriteを使って再実装できますが、(実際に自分も42の課題でprintfの再実装に取り組んでいます)write関数を再実装するというのは、私は今までに聞いたことがないです。
Hello Worldはこんな感じ。
#include<unistd.h>
int main(void)
{
write(1,"Hello World\n",12);
}
出力
Hello World
まず第一引数で、出力する場所を指定します。今回の場合は1なので標準出力を指定しています。何を言っているのかよくわからないかもしれませんが、数字で出力する場所を色々いじれるということです。
今回は難しすぎるので割愛しますが、詳しく調べたい方はファイルディスクリプタと検索してみてください。
第二引数には出力したいデータのアドレスを入れます。今回の場合は文字列を出力したいので、Hello World
という文字列の後に改行を指示する文字\n
を入れました。
第三引数は何バイト分出力したいのかを入れます。まあ文字数入れとけば大丈夫です。
今回はprintfの説明がメインなので割愛しますが、write関数の方がprintfより、余裕で深掘りできる部分が多く、全てを理解するのはとても難しいです。まあ私は流石に、write関数完全に理解した() けどね
(追記)
割愛するとは言いましたが、流石に戻り値くらいは説明しないといけないですね。普通に忘れてました。
#include<unistd.h>
#include<stdio.h>
int main(void)
{
ssize_t len;
len = write(1,"Hello World\n",12);
printf("%zd\n",len);
}
出力
Hello World
12
実際に出力された文字数が返ってきます。書き込みに失敗した場合は-1が返ってくるので、エラーハンドリングは割と簡単かなと思います。
printf
printfの方も見てみましょう。
実際にHello Worldを書くとこんな感じ。
#include<stdio.h>
int main(void)
{
printf("Hello World\n");
}
出力
Hello World
printf関数の第一引数に文字列を指定すれば、その文字列を標準出力に出力してくれる。
内容自体はwriteのものと同じですね。
しかし、実はprintfの真の価値はもっと他に存在するんです。
#include<stdio.h>
int main(void)
{
char *str = "Hello World";
printf("%s\n", str);
}
こんな書き方はどうでしょう。
何やら見慣れない%s
という文字列が出てきましたが、これでも出力はHello World
になるんです!
今回のコードでは、まずchar *型(チャーポインタ)でstrを宣言し、HelloWorldを代入しています。
charというのは本来、1文字を表すのに使う型ですが、C言語には文字列型がないため、これ*
をつけることで、文字の塊を表しています。これ*
のことをポインタと言って、ポインタで色々メモリを操作したりできるのですが、一旦は文字列型がないから、文字の塊として処理するんだー、くらいの認識でOK!
ポインタやCの文字列については一旦この辺がおすすめ
C言語のポインタを日本一わかりやすく解説する〜導入編〜
【C言語入門】文字・文字列(char)の使い方
その後に出てくるのがprintf("%s\n", str);
このコードが何をしているのかというと、%sがある場所に、変数strに代入されている、"Hello World"
を埋め込んで、出力するというもの。
実はprintf関数は、フォーマット指定子というものを使って、文字列中に出てくる%よりの次に出てきたフォーマット指定子に応じた型から、2番目以降の引数に指定されたデータを文字列に変換して、補完しそのまま出力してくれるという関数でした。
主に使用するフォーマットフォーマット指定子はこの辺り
指定子 | 対応する型 |
---|---|
d | 符号付き10進数(int) |
s | 文字列(char配列) |
c | 文字(char) |
p | ポインタのアドレス(void*) |
% | パーセント記号(%) |
ちなみにフォーマット指定子の数に制限はないのでこんなこともできたりします。
#include<stdio.h>
int main(void)
{
printf("Hello! I'm %s! My blood type is %c and I'm %d years old!\n", "hogehoge", 'A', 20);
}
出力
Hello! I'm hogehoge! My blood type is A and I'm 20 years old!
ところで、printfのprintの部分はもちろん印刷を意味しますが、fはどのような意味を持つのでしょうか、?
ここまで、連呼していたらお気づきの方もいらっしゃるかもしれませんね。
正解は...
フォーマット、つまり書式、形式を意味します。
つまりprintfはただの出力する関数ではなく、 指定した形式で文字を出力できる関数だったというわけです。
今まで何気なく使っていたprintfにもこんな意味が含まれていたんですね〜。
終わりに
実は普段何気なく使っているものも、思っているより深い意味や理由があったりします。それはプログラミングだけに限った話ではありません。
全てのことに疑問を持ち、探究することで見えてくる世界もたくさんあるのではないでしょうか
そんな、思考を止めずに考え続けることが大好きな方々へおすすめしたいのがC言語です!!
C言語自体、古い言語に分類されるため、最適化がされていなかったり、なんでこのように書いてあるんだろう?という部分が多数あったりします。その理由を知ったり、気がついた時の楽しさは他の言語と比べても圧倒的です!(当社比)
プログラミングを楽しみたい人、プログラミングが大好きな人、プログラミングが手段ではなく目的になってしまっている人は、ぜひ一度触ってみてくださいね!!
おまけ
本来はこれが書きたかったという話。何ならこっちが本編。
突然ですがprintfとwriteで異なる部分って何だと思いますか?
フォーマット機能?出力先?サイズ?
それももちろん正解ですが、他にも違う点があります。それは、実装方法。
もっというと出力の方法とタイミングです。
こんなコードを試してみましょう。
#include<unistd.h>
#include<stdio.h>
int main(void)
{
char *str = "shita";
write(1,"tsukar",7);
printf("%s",str);
write(1,"ema",3);
printf("\n");
}
このコードの出力結果はどうなると思いますか?
単純に考えれば、上から順番に実行されて、
tsukarshitaema\n
となりそうですが、実際の出力は違います。
実はこれをコンパイルして実行すると
tsukaremashita\n
となります。
このようになる原因が、先ほども述べた、printfとwriteの出力方法の違いです。
write関数はシステムコールをAPIから直接呼び出して実行して、関数を読んだ瞬間、標準出力に書き込まれてコンソールに表示されます。
一方でprintf関数はデータを直接送受信せず、一時的にバッファに貯めておき、一定量に達したときまとめて送信するバッファリングという処理を行っています。
このまとめて送信するタイミングが、関数が呼ばれた直後ではないため、このような挙動が起こります。
具体的にprintfがバッファリングした文字列を送信するタイミングは以下の通りです。
- バッファサイズを上回る文字数を入力する(多くの場合は4096文字が上限らしい)
- 改行文字を入力する
- fflush(stdout)を使う
- プロセスが終了する
先ほどの場合だと、
- 1つ目のwriteが呼ばれ、
tsukar
が出力されます(結果tsukar
) - 1つ目のprintfが呼ばれ、バッファに
shita
が記録されます(結果tsukar
) - 2つ目のwriteが呼ばれ、
ema
が出力されます(結果tsukarema
) - 2つ目のprintfが呼ばれ、バッファに
\n
が記録されます(結果tsukarema
) - バッファに改行が入ったため、バッファの内容が出力されます。(結果
tsukaremashita\n
)
そのためたとえば、1つ目のprintfの文字列に改行文字が入っていたりすると、挙動が全然変わってしまったりします。
#include<unistd.h>
#include<stdio.h>
int main(void)
{
char *str = "shita";
write(1,"tsukar",7);
- printf("%s",str);
+ printf("%s\n",str);
write(1,"ema",3);
printf("\n");
}
このように変更すると、出力は上から順番になり
tsukarshita\nema\n
となるわけです。
printfとwriteの出力方法の違いを見えるようにする他の方法
たとえばこんな方法を試してみましょう
#include<unistd.h>
int main(void)
{
int returnP;
int returnW;
close(STDOUT_FILENO); //stdoutを閉じる
//returnを代入
returnP = printf("abcde");
returnW = write(1,"abcde",5);
//標準エラー出力にreturnを出力
fprintf(stderr,"PRINTF:%d\n",returnP);
fprintf(stderr,"WRITE:%d\n",returnW);
}
printfとwriteの戻り値は、成功時は出力したバイト数、失敗時は-1が返されます。
今回は入力先である、stdoutを閉じて、戻り値を標準エラー出力に表示してみました。
本来であれば、どちらも出力が失敗されて-1が出力されそうですが、実際の出力はこうなります。
PRINTF:5
WRITE:-1
もちろん標準出力が閉じているため、どちらも文字列の出力は失敗していますが、printfの方はなぜか5が戻ってきています。
おそらくですがprintfは 実際に出力した文字数ではなく、バッファリングした文字数を戻している のではないかと考えられます。出力には失敗しているのに、その前バッファに文字を詰めていく工程は成功しているためそこの文字列を数えてreturnした結果、このような出力になっているのではないでしょうか?
やはり、printfは面白いですね!!
by とあるプロクラTA