C言語で一度挫折した人が読むnote①

私は毎年春に新入社員研修の講師としてC言語を教えています。
そんな講師の目から見て、C言語を習得できる人と、そうでない人の違いをいくつか紹介します。

ここから書くことはとても大切な事なのですが、意外とすっ飛ばしてしまいそうなことばかりです。しかしC言語を習得するうえではとても大切な要素なので、一つ一つ読んでみてください。一度C言語で挫折した人が、再度C言語にチャレンジしてみようと思える内容になっていると思います。

ちなみにC言語に興味・関心がない人や、一度もC言語を勉強したことがない人には、まったく意味の分からない内容です。

C言語を詳しく知っている方にとっては、私の説明は無茶苦茶だと思われるかもしれません。しかし、あくまでもC言語に一度挫折した人が、「あーそうなんだ」と思えるようにと書いたものです。そこは、説明が少々おかしくてもご容赦くださいね。

さてさて、はじめましょうか。

1.char型は文字を入れるものではない

変数の型名が「char」なんていう誤解を招きやすいネーミングなのが悪い。私はそう感じます。確かに文字を代入することは可能ですけど……

char型の正体は1バイトの整数型です。
(実際は1バイトの符号なし整数型です)

そもそもコンピュータの内部では、すべてのデータとプログラムが2進数に変換されて扱われます。

つまり、コンピュータにとっては、すべての情報は2進数です。でもそれだと人間は理解できないし、0と1の並びだけでプログラミングすることなんて不可能です。だから、ほとんどのプログラミング言語にはデータ型(以下、単に型と記載)があるのです。

じゃあ、型ってなんぞや?と考えると、次の3つの概念だと思ってください。(かなり乱暴な説明かもしれませんが)

  1. そのデータのサイズは?

  2. 代入する時、どのように変換するか?

  3. 取り出すとき、どのように変換するか?

例えばchar型の場合は、

  1. サイズは1バイトと定義されています。

  2. 代入する時は数値に変換されます。

  3. 取り出すときも数値に変換されます。

例えば次のようなコードがあるとします。

	char ch1;
	ch1 = 'a';

変数ch1は、char型で作成されます。
ですから、サイズは1バイトですね。
で、代入する時は、'a'の部分を数値に変換してch1に代入します。

シングルクォーテーション(')で括られた1文字は、人の目から見たら文字に見えますが、コンピュータにとっては単なる数値でしかありません。

この文字'a'は、私たちが良く使う10進数にすると97です。
乱暴な言い方をすると、ch1には、'a'ではなく97が代入されるわけです。
最終的には97が2進数に変換されて、01100001がch1に書き込まれるわけですね。

もちろん、上の説明は人間がわかりやすく順を追っていますが、実際には'a'がいきなり01100001になってch1に書き込まれます。

char型には確かに文字を代入できますが、char型にしか文字が代入できないわけではありません。

次のプログラムを見てみましょう。

	int ch2;
	ch2 = 'b';

おっと、今度はint型でch2を宣言しています。
int型は環境によってサイズが異なりますが、一般的な32ビットで考えてみましょう。

今度は'b'を代入していますね。(ch2='b' のところ)
しかし、今度は=の左側の代入先がint型です。
つまり、'b'を4バイトにしてからch2に代入されます。

ちなみに'b'を10進数にすると98です。
1バイトの2進数にすると、01100010です。
このままでは、代入先とサイズが異なるため、サイズ合わせを行います。

C言語は、代入先の方がサイズが大きい場合は自動型変換が行われます。
今回の場合、代入先(ch2)が4バイト、代入元('b')が1バイトですから、自動的に4バイトにサイズ調整されます。

その結果、'b'は、
0000 0000 0000 0000 0000 0000 0110 0010
となります。
(見やすいように4ビットずつスペースで区切っています)

つまり=の左辺であるch2には、
0000 0000 0000 0000 0000 0000 0110 0010
が入っていることになります。

ちょっと説明が長くなってしまいましたが、int型にも文字を代入することは可能なわけです。

ここまで読んでいただければ、「char型は文字を入れるものではない」という理由がわかっていただけたのではないでしょうか。

文字も入れられるけど、中身は数値!!なんです。

char型という名前に騙されそうになりますよね。

ちなみに、char型のch1も、int型のch2もprintfの%cで表示すると、きちんと文字として表示されます(aとbですね)

	printf("ch1を文字として表示すると %c\n", ch1);
	printf("ch2を文字として表示すると %c\n", ch2);

実行結果は、こんな感じ

ch1を文字として表示すると a
ch2を文字として表示すると b

おっと、説明を1つ省略していました。
先に「3.取り出すとき、どのように変換するか?」ということも書いていましたね。

ここで、上記のprintfの例を見てみましょう。

1行目のch1を表示している方は、printf関数に引数としてch1を渡しています。

受け取ったprintfさんは「ch1はchar型だね!だったら、1バイトだけ読み込んで0と1の並びをchar型として解釈すればいいんだね!」と理解してくれます。

同様に2行目のch2を表示している方は、引数がint型なので、賢いprintfさんは「ch2はint型か!だったら4バイト取り出して、0と1の並びをintとして理解すればいいんだね!」と考えて実行してくれます。

ちょっと3.の説明がかるくなってしまいましたが、そういう動きをしていくれています。

厳密にはch2はunsigned intとして処理されていますが、まあ、細かいことは一旦置いておきましょう。

2.配列名と文字列リテラルは、アドレスである

はい。これも大半の挫折者が誤解するポイントです。

次のコードを見てください。

char	str[20];
str = "abc";

初心者のほとんどが一度はやってしまうミスです。

2行目の「str="abc";」がコンパイルエラーになります。

strは要素20の配列だから20文字分格納可能です。
普通に考えれば、"abc"は3文字だから代入可能なように思いますよね。

しかし、できません。

実は2行目には、C言語の大切なルールが2つ隠されています。

1つ目は、配列名はアドレスである。ということ。
厳密には配列要素0番目のアドレスです。
つまり、str[0]がメモリ上のどの場所にあるかという情報と等価なのです。
仮にstr[0]が1000番地にあったとしますね。この仮定を覚えておいてください。

次に2つ目のルール。
それは、文字列リテラル(""で括られた文字列のこと)はアドレスである。ということです。

つまり"abc"は、見た目とは異なりアドレスを書いているのと同じことなのです。C言語で文字列リテラルを記述すると、コンパイル時にメモリ上にa、b、c、\0 の4文字の配列が作成されます。
その先頭のアドレスと"abc"が等価ということになります。
仮に"abc"が2000番地に用意されたとします。
雑に書くと、"abc"と書くと2000と書いたのと同じような意味になります。

では、コンパイルエラーになった行を再確認。

str = "abc";

う~ん。=の左辺も右辺もアドレスということになってしまいます。
例えば、皆さんのお住まいには個別の住所が必ずありますよね。
その住所を勝手に書き換えられたらどうします?
当然困りますよね。郵便物が届かなくなりますし、役所の人は大変困るはずです。運転免許証や健康保険証の住所欄も意味をなさなくなります。
そんな大胆不敵な行動を起こそうとしているのが……

str = "abc"; ってわけなんですよ。

つまり、2つの仮定から考えると、上の式は「1000番地を2000番地に書き換える」という作業をしようとしています。

1000 = 2000; って書いたのと同じような意味です。

言うまでもなく、メモリというのはコンピュータ内部にある物理的な装置です。物理的ということは、みなさんの住居と同じように、勝手に住所を書き換えられると困るのですよ。

今まで1000番地だったところが、プログラムの中で突然2000番地になってしまったらどうしますかね。実生活で考えるなら、ある日突然、東京都が埼玉県に変わってしまうみたいなことです。

で、こまったことに、ここで紹介した2つのルールである「配列名はアドレス」「文字列リテラルはアドレス」ということを、初心者向けの入門書では触れていないことがあるんですよね。

とても重要な事なのですが、初心者向け書籍の学習順序から考えると仕方のないことかもしれません。だって、本丸であるポインタを学習するのは、ずっと先のことになるのですから。

ちなみに配列名、文字列リテラル以外にもアドレスを表している表記があります。それは関数名です。

初心者の内は「関数名(引数リスト)」という書き方しかしませんよね。しかし、上級になってくると、関数のアドレスを使うようになります。その時に「(引数リスト)」を書かない場合があります。それが関数のアドレスを表します。割り込み処理なんかの時に使うので、初心者の内は縁がないかもしれません。ご参考までに。

3.*には2つの意味がある

ついにポインタに触れる時が来てしまいました。
C言語最大の難関であり、他のプログラミング言語には登場しないポインタです。

多くの初心者の心をズタボロにし、挫折させてきたポインタです。

そして、大抵の初心者が誤解している(というか理解していない)のが「*」の2つの意味です。

それは、変数宣言時とその後では「*」の意味はまったく異なる。ということです。

こんなプログラムがあります。

	int* p;
	int	a;
	p = &a;
	*p = 5;

1行目の「int* p;」の「*」と4行目の「*p = 5;」の「*」はまったく別ものです。根本的に異なるものだと考えてください。

1行目の方は変数宣言時の「*」です。
変数宣言時は、「pという変数は、アドレスしか格納しちゃだめよ!」という意味の「*」です。

4行目の方は変数宣言時ではないので「pに格納されているアドレスを見に行ってね。そのアドレスを操作するんだよ」という意味の「*」です。

同じ「*」なのに使う場所で意味が全然異なるので、初心者は混乱してしまいます。

では順に見ていきましょうか。

まず1行目。
pという名前の変数が作られます。ただし、pの中身は空です(というか保証されません)。つまりどこも指していないポインタ変数ができるというわけですね。
pと言う名前のポインタ変数そのものは1000番地に作成されたとしましょう。1000番地にpという名前の変数は作成されるが、中身は空っぽという状態です。

次に2行目。
int型の変数aが作られます。
仮にaのアドレスを2000番地としましょう。
初期化していないので、aの中身も保証されません。

そして3行目。
p = &a; です。
&演算子を使うと、変数のアドレスを取得することができます。
つまり、pに変数aのアドレス(2000番地)が代入されます。
この時点でaには何が入っているかは保証されていません。
pにはaのアドレスが入っています。

最後の4行目。
*p = 5; です。
この行は変数宣言ではないので、「*」は「pに入っているアドレスを見に行け」という意味です。
つまり「2000番地を見に行け」「そしてそこに5を代入しろ」ということですね。
この結果、2000番地には数値の5が代入されます。
2000番地って、aのアドレスですから、結果的にaに数値5が代入されます。

実際には、こんなまどろっこしいやり方でポインタ変数を使うことはないのですが、あくまでも勉強材料です。

このように、変数宣言時の「*」とそれ以外の「*」は意味が違うのです。
このことをしっかりと理解しておかないと、このあと関数の引数にアドレスを指定した際などに戸惑うことなるんですよね。

ちなみに「*」には、乗算(かけ算)の意味もあります。念のため。

ということで、ここまでのことだけでも5,000文字を越えてしまいました。
評価が高ければ、続きを別途書こうと思います。

一応、今後書こうと思っていることの一覧を下記に。

4.仮引数と実引数は完全に無関係

5.NULLと\0は別のもの

6.スコープ

7.記憶クラス

8.マシン語になるまで(ビルドとかコンパイルってなんだ?)

9.プロトタイプ宣言って?

10.#includeってなんじゃ?

いただいたサポートは、おじさんの活動費としてとんでもなく有用に使われる予定です。