配列がポインタに自動変換され、添字アクセスがポインタへの算術演算を通じたアクセスと等価と定義し、同じもののように扱える、というアイデアを捨て、普通に別のものとして扱うようにする。
この仕様は、配列の範囲外アクセスチェックをするような言語拡張を事実上不可能にし、char,char*が日常的に使わなければならない「文字列」でもあると同時に「任意のバイト、バイト領域を扱えるオールマイティ」という高危険度データ型と不可分同一、という仕様とあいまって、「安全な文字列操作を、並みの人間では長期的にはほとんど不可能にする」という結果を招き、今となっては凄まじいクソ仕様というしかありません。現代でもバッファオーバーフロー攻撃による脆弱性の発見が後をたちませんが、元凶はほとんどここらへんと言っても差し支えないと思います。
まあ、当時はネットワークを通じた攻撃という概念がほとんどなかった頃なので、まさしく後知恵ですが、時空を越えた憑依ということで。(畏れ多い!)
もちろん、C言語は紛れもなく素晴らしい発明であり、人類文明に多大な貢献を果たしたことは、感謝と共に申し添えておきます。
creat 関数を create にする…というのは、Ritchie の言ったジョーク(?)ですが、PDP シリーズの乏しいリソース、容量が小さくて遅いハードディスク、今時のプリンタなど比較にならない低速のテレタイプ I/O コンソール、ということを考えると、何を提起しても馬鹿げていると一蹴されたかなと思います。
int がマシンによって 16ビットになったり 32ビットになったりしたのは、PDP シリーズや VAX シリーズとの互換性を考えるとやむを得なかったことですし、当時の低速な PDP では、32bit 値を扱うのはコストのかかる、つまり遅い処理になってしまっていたためです。じゃあ int16 とか、int32 を導入すれば、という意見もあるようですが、そうすると、UNIX のポータビリティが失われるのでデメリットしかなかったのです。また、BCPL のサブセットとしての B 言語、その発展としての C 言語を考えると、1ワードが必ずしも 8ビットや 16 ビットとは限らなかった当時のコンピュータを考えればビット数の規定はむしろ邪魔と言われたと思います。
また、配列の添え字チェックをしないとか配列とポインタの互換とかは当時としては苦肉の策で、チェックを入れたり分離したりすればただでさえ遅いマシンに遅い処理系が走ることになって開発者としては認められなかったでしょう。
直接走るコードを書けるからです。
PythonとかJavaとかが動くようにするためには、それを走らせる環境が必要で、順当に考えるとOSが載るような物が必要になる。 組み込みの一部にはOSが乗っていますが、小規模なデバイスはOSが無く、直接コードが走る必要があります。
OSが要らず、プログラムがコンパクトで速ければ安いデバイスで済むわけです。
例えばちょっとした製品に、OSが走る300円の32bitの高性能ARMマイコンではなく、30円の16ビットマイコンにすれば原価が大幅に下がります。 プログラム開発に多めのコストを吐き出したところで、1台1台の製品を作るときには270円ずつ安くなり1万台で270万円安くなります。 更に、小規模なデバイスは電源や周辺制御、基板や実装コストも安くなるのでこの価格差は拡大します(OSが動くようなチップは大抵BGAという実装で多層基板が必要ですが、小規模な物なら1層でもなんとかなるし両面程度で十分になる)
ソフトウェア製品は、ソフトだけ売れば良くネットの時代になって量産コストがほぼ皆無だから開発コストだけ気にすれば良いが、組み込みは対象の製品を作れば作る分だけ材料の代金がかかる。
コンパクトなLinuxが動くボード
Linuxをそれなりに動かすなら、これの中央にあるMPUと左にあるメモリチップくらいは必要。 そして、それの電源やクロック回路等が上下と更に
直接走るコードを書けるからです。
PythonとかJavaとかが動くようにするためには、それを走らせる環境が必要で、順当に考えるとOSが載るような物が必要になる。 組み込みの一部にはOSが乗っていますが、小規模なデバイスはOSが無く、直接コードが走る必要があります。
OSが要らず、プログラムがコンパクトで速ければ安いデバイスで済むわけです。
例えばちょっとした製品に、OSが走る300円の32bitの高性能ARMマイコンではなく、30円の16ビットマイコンにすれば原価が大幅に下がります。 プログラム開発に多めのコストを吐き出したところで、1台1台の製品を作るときには270円ずつ安くなり1万台で270万円安くなります。 更に、小規模なデバイスは電源や周辺制御、基板や実装コストも安くなるのでこの価格差は拡大します(OSが動くようなチップは大抵BGAという実装で多層基板が必要ですが、小規模な物なら1層でもなんとかなるし両面程度で十分になる)
ソフトウェア製品は、ソフトだけ売れば良くネットの時代になって量産コストがほぼ皆無だから開発コストだけ気にすれば良いが、組み込みは対象の製品を作れば作る分だけ材料の代金がかかる。
コンパクトなLinuxが動くボード
Linuxをそれなりに動かすなら、これの中央にあるMPUと左にあるメモリチップくらいは必要。 そして、それの電源やクロック回路等が上下と更に裏面に並んでいる。 左のUSBと右のLANは不要なら外せるが、それを切り詰めても半分くらいのサイズは残る。 メモリは512MB等で、クロックは1GHz等。 しかし、メモリもCPUの計算能力もOSにだいぶ持って行かれる。
直接走るコードを動かすならこんなボードで出来る
大きいのがMPUで内部にメモリも持っていて、外部には電源とかが少しあるだけ。さっきのLinuxボードの右上にあるチップがこのMPUと同じサイズ。 数十MHzの計算コアと、数百KBのプログラムフラッシュと数百KBのメモリがチップの中に収まっているのでCで書いたコンパクトなプログラムはそれを全て独占し、チップ1個で完結出来る。
型チェックも組み込みの文字列型も多重インクルードを防止するディレクティブもなくて構わない.そういう時代だった.
だがセミコロン,てめぇだけは駄目だ.セミコロンはデリミタじゃなくてセパレータであるべきだったんだ.
脚注
検索しやすい言語名にする。
わかりやすく、と言うならば。
表計算(Excelなど)で例えてみます。
Excelを使用する時、何気なく使っているセルにも必ずA1やA2などのアドレスが存在していることと似ています。
例えば、A2セルに=A1と入力すれば、A1セルの値がA2セルにも表示されます。
これはポインタ変数にアドレスを入れているのと似ています。
- #include <stdio.h>
- int main(void){
- int A1;
- int *A2;
- A1=9999;
- A2=&A1; //excelでいうところの、=A1と同じ。
- //%d 数字を表示、A2はポインタ型なので不可能
- printf("A1 value:%d\r\n",A1);
- //printf("A2 value:%d\r\n",A2);できない。
- //%p Addressを表示、A1はint型なので不可能
- //printf("A1 value:%p\r\n",A1);できない
- printf("A2 value:%p\r\n",A2);
- //%p Addressを表示、両方可能
- printf("A1 address:%p\r\n",&A1);
- printf("A2 address:%p\r\n",&A2);
- //%d pointerを表示、A1はint型なので不可能
- //printf("A1 pointer:%d\r\n",*A1); できない。
- pr
わかりやすく、と言うならば。
表計算(Excelなど)で例えてみます。
Excelを使用する時、何気なく使っているセルにも必ずA1やA2などのアドレスが存在していることと似ています。
例えば、A2セルに=A1と入力すれば、A1セルの値がA2セルにも表示されます。
これはポインタ変数にアドレスを入れているのと似ています。
- #include <stdio.h>
- int main(void){
- int A1;
- int *A2;
- A1=9999;
- A2=&A1; //excelでいうところの、=A1と同じ。
- //%d 数字を表示、A2はポインタ型なので不可能
- printf("A1 value:%d\r\n",A1);
- //printf("A2 value:%d\r\n",A2);できない。
- //%p Addressを表示、A1はint型なので不可能
- //printf("A1 value:%p\r\n",A1);できない
- printf("A2 value:%p\r\n",A2);
- //%p Addressを表示、両方可能
- printf("A1 address:%p\r\n",&A1);
- printf("A2 address:%p\r\n",&A2);
- //%d pointerを表示、A1はint型なので不可能
- //printf("A1 pointer:%d\r\n",*A1); できない。
- printf("A2 pointer:%d\r\n",*A2);
- return 0;
- }
結果は以下のようになります。
- A1 value:9999
- A2 value:0x7fff53919838
- A1 address:0x7fff53919838
- A2 address:0x7fff53919830
- A2 pointer:9999
0xではじまるのが実際のアドレスです。
よく見るとA1とA2のaddressの末尾が若干異なっています。
(3行目と4行目です。)
0xで始まるアドレスをExcelのアドレスに置き換えると以下のようになります。
- A1 value:9999
- A2 value:A1
- A1 address:A1
- A2 address:A2
- A2 pointer:9999
まとめ:
ポインタを理解するときは、Excelに当てはめれば視覚化されてわかりやすくなるかもしれないです。
理解の助けになったら幸いです。
逆にポインターはなぜ必要なのかを考える方が早いかも知れません。
コンピューターのプログラムはメモリに書き込まれて動いています。
BASICなどのプログラムは使う人がメモリを意識しません。どのメモリーのアドレスに保存されていても気にしないわけです。
ただし、C言語はそれでは困るのです。例えば下の方に書いてある「H/8 IO領域」というのはメモリ上に配置されたコンピューターを制御する特別なメモリです。読んだり、書いたりすることでCPUを制御できます。
メモリの値を直接読んだり、書いたりできるようにしたのが「ポインター」です。
例えばメモリの0xFFFF10に0xFFを書き込まないといけない場合、
char *a; とポインター型変数を宣言します。 ポインターとはアドレスを代入できる変数なのです。
a=0xFFFF10; とポインターにアドレスを代入します。
*a = 0xFF; と書くことで、アドレス0xFFFF10に0xFFを書くことができます。
char a;
char *b;
と宣言した時、aと*bが同じように変数として扱えます。
a=10;とした時
アドレス 値
0000 00
0001 00
0002 10 ←a
0003 00
0004 00
変数a にはアドレスと値の2つの情報があります。
変数bは*bと書けば値、bと書けばアドレスを保存できます。
b=0003;
とbにアドレスを入れて
*b=2
逆にポインターはなぜ必要なのかを考える方が早いかも知れません。
コンピューターのプログラムはメモリに書き込まれて動いています。
BASICなどのプログラムは使う人がメモリを意識しません。どのメモリーのアドレスに保存されていても気にしないわけです。
ただし、C言語はそれでは困るのです。例えば下の方に書いてある「H/8 IO領域」というのはメモリ上に配置されたコンピューターを制御する特別なメモリです。読んだり、書いたりすることでCPUを制御できます。
メモリの値を直接読んだり、書いたりできるようにしたのが「ポインター」です。
例えばメモリの0xFFFF10に0xFFを書き込まないといけない場合、
char *a; とポインター型変数を宣言します。 ポインターとはアドレスを代入できる変数なのです。
a=0xFFFF10; とポインターにアドレスを代入します。
*a = 0xFF; と書くことで、アドレス0xFFFF10に0xFFを書くことができます。
char a;
char *b;
と宣言した時、aと*bが同じように変数として扱えます。
a=10;とした時
アドレス 値
0000 00
0001 00
0002 10 ←a
0003 00
0004 00
変数a にはアドレスと値の2つの情報があります。
変数bは*bと書けば値、bと書けばアドレスを保存できます。
b=0003;
とbにアドレスを入れて
*b=20;
と代入すると
アドレス 0003に20が入ります。
アドレス 値
0000 00
0001 00
0002 10 ←a
0003 20←*b
0004 00
&を使うと、アドレスを調べることができます。
&aは0002になります。
b=&a とするとbのアドレスは0002になります。
その状態で*b = 30;とすると変数aの値を書き換えれます。
アドレス 値
0000 00
0001 00
0002 30 ←a ←*b
0003 20
0004 00
・・・
例えばメモリーを配列だと想像してもらうと、アドレスというのは配列の添え字になります。
char MEMORY[1000000];という大きな配列があると思ってください。
char *y; とポインターを宣言すると
*yというのはMEMORY[y]の配列の添え字を操作するイメージです。
y=100;
*y=10;
a = *y;
というのは、
y=100;
MEMORY[y]=10;
a=MEMORY[y];
と同じことです。
「*」は「MEMORY[ ]」の省略だと思ってください。
凄くイメージ重視の説明ですが。
C言語のポインタについてのモヤモヤが解消した印象深い思い出があります。
ン十年も前、まだ中学生か高校に上がったばかりの頃だと思います。当時、ほぼ定期購読していたマイコン雑誌があって、ある号から私にとって革命的な連載が始まりました。
その頃は8ビットCPUのZ80やMC6800などで組んだ自作ハードウェアに16進でマシン語をちまちま打ち込んでいた時代です。そんな時、「自作のハードウェアなんてもう卒業しよう!これからはプログラミングを楽しむ時代だ!」と銘打った企画が登場したのです。
連載テーマは、当時少しは知られるようになったC言語のコンパイラの製作です。Cコンパイラ(のサブセット)をC言語でコーディングしていくのです。私にとって、実質的なプログラミングキャリアのデビューは、連載されたC言語のソース(しかもコンパイラという得体のしれない代物が相手)をひたすら熟読C言語を学びながらコンパイラの作成技法を学ぶことでした。
でも結果的にはそれが正解だったのです。
C言語のポインタを学んだ頃に抱いた疑問・誤解、およびそれらを克服するきっかけは何でしたか?に対するOgawa Kiyoshiさんの回答 でも似たような経験が語られていますが、特定のプログラミング言語に精通したければそのプログラミング言語の処理系(コンパイラ、インタプリタ)の中身を勉強するに限るということです。
ちょっと、その時のAh Hah!体
C言語のポインタについてのモヤモヤが解消した印象深い思い出があります。
ン十年も前、まだ中学生か高校に上がったばかりの頃だと思います。当時、ほぼ定期購読していたマイコン雑誌があって、ある号から私にとって革命的な連載が始まりました。
その頃は8ビットCPUのZ80やMC6800などで組んだ自作ハードウェアに16進でマシン語をちまちま打ち込んでいた時代です。そんな時、「自作のハードウェアなんてもう卒業しよう!これからはプログラミングを楽しむ時代だ!」と銘打った企画が登場したのです。
連載テーマは、当時少しは知られるようになったC言語のコンパイラの製作です。Cコンパイラ(のサブセット)をC言語でコーディングしていくのです。私にとって、実質的なプログラミングキャリアのデビューは、連載されたC言語のソース(しかもコンパイラという得体のしれない代物が相手)をひたすら熟読C言語を学びながらコンパイラの作成技法を学ぶことでした。
でも結果的にはそれが正解だったのです。
C言語のポインタを学んだ頃に抱いた疑問・誤解、およびそれらを克服するきっかけは何でしたか?に対するOgawa Kiyoshiさんの回答 でも似たような経験が語られていますが、特定のプログラミング言語に精通したければそのプログラミング言語の処理系(コンパイラ、インタプリタ)の中身を勉強するに限るということです。
ちょっと、その時のAh Hah!体験をシェアしてみたいと思います。
C言語の学習者にとってポインタは鬼門と言われています。特にポインタと配列とが混淆していることが問題で、初心者に限らず、次の3つの宣言文が何を宣言しているのかについては中級者でもすぐには答えられません。
- int **a; // (1)
- int *b[3]; // (2)
- int (*c)[3]; // (3)
正解は、
- (1)の "int **a;" は、aという名前(変数名)の変数を宣言しており、
- その型は、intオブジェクトへのポインタへのポインタ
- (2)の ”int *b[3];" は、bという名前(配列)の配列を宣言しており、
- 配列サイズは3で、
- 各要素の型はintオブジェクトへのポインタ
- (3)の ”int (*c)[3];” は、cという名前(変数名)の変数を宣言しており、
- その型はサイズ3のintオブジェクトの配列へのポインタ
図で書くと、こんな感じです。黄色で塗りつぶした部分が宣言対象のオブジェクトで、それらには変数名や配列名が付けられています。
では、これがコンパイラの中身とどのように関係しているのでしょうか?
カギは、「C言語の演算子の優先順位」と「Cコンパイラが内部生成する構文解析木データ」にあります。
まずは、演算子の優先順位について。
C言語では、式中での配列の添字演算子[]はポインタの間接参照演算子*よりも優先度が高く、宣言文でも同じ優先順位に従います。()を使って、優先関係を明示的に表せば、以下のようになります。
- int **a; // (1) → int *(*a);
- int *b[3]; // (2) → int *(b[3]);
- int (*c)[3]; // (3) → int (*c)[3];
次に、コンパイラについて。
Cコンパイラの前半部の処理は、C言語のソースコード(テキストファイル)を読み込み、C言語の文法に基づいて構文解析を行い、内部に構文解析木データを構築することです。後半処理では構築した構文解析木データの意味を解釈し、マシン語に翻訳します。
宣言文に対応する構文解析木データを作成するアルゴリズムは非常に単純です。ポインタと配列が絡む部分だけを抜き出せば、次の2つのルールだけです。
- ルール1: T | *x; ⇒ {pointer-to {T}} | x;
- ルール2: T | x[n]; ⇒ {array[n]-of {T}} | x;
矢印(⇒)の左側のパターンが現れたら右側のようなパターン(=構文解析木データ)に置き換えるのです。なお、縦棒(|)は型指定子(=型情報)と宣言子(=宣言対象)とを区切るための便宜上の印です。
実際に、例(1)で試してみます。まず、ルール1を適用すれば以下の通り。
- // (1) int **a;
- int | *(*a); ⇒ {pointer-to {int}} | *a;
再度、ルール1を適用して
- {pointer-to {int}} | *a; ⇒ {pointer-to {pointer-to {int}} | a;
縦棒(|)の右側が識別子(名前)だけになれば、構文解析木データは完成です。読んで字の如く、aが「(int へのポインタ)へのポインタ」であることは明白です。
例(2)(3)も同様です。それぞれ、以下の通りです。
- // (2) int *b[3];
- int | *(b[3]); ⇒ {pointer-to {int}} | b[3];
- {pointer-to {int}} | b[3]; ⇒ {array[3]-of {pointer-to {int}}} | b;
- // (3) int (*c)[3];
- int | (*c)[3]; ⇒ {array[3]-of {int}} | *c;
- {array[3]-of {int}} | *c; ⇒ {pointer-to {array[3]-of {int}}} | c;
縦棒(|)をイコール(=)と見做すと、まるで方程式を解いていくような感覚です。実際にもコンパイラ内部ではこのような処理を行なっているわけですが、人が紙の上で行うことも簡単です。英語が分かれば、何が宣言されていて、その型が何なのか一目瞭然でしょう。
もっと複雑な宣言文も、丹念に方程式を解いていけば、いずれ解が得られます。
逆もまた然りで、こういう型のオブジェクトを宣言したいと思ったら、逆方向にパターンを書き換えていけばよいのです。
こうしたことを、コンパイラのソースを読みながらはたと気がついた次第です。
まぁ、こういった経緯もあってか、私はポインタの悪夢からは早ばやと卒業しました。もちろん、マイコン少年として、ハードウェアやアセンブリ言語の知識もそれなりに持っていたので、そのことも助けになった部分はあります。
実は白状すれば、ポインタはもう大丈夫なんですが、const や volatile などの型修飾子が結構厄介で、目下の頭痛のタネになっています。
Cのセミコロンは文の終わりの記号ですが、せっかくなのでプログラミングでのセミコロンの歴史に関して以下の記事を参考にしながら考えてみたいと思います。
英語でのセミコロンの意味ですが、コンマ(,)より強くピリオド(.)より弱い文の区切りだそうです。
上のサイトで例文として
I went to the library; Jonathan went to the theater.
「私は図書館に行き、ジョナサンは劇場に行った」
が挙げられていてセミコロンの前と後ろは独立の文で、しかも関連が強い場合に使うようです。
さてプログラミングの話に進めましょう。
世界で初めての高級言語はFORTRANと言われています。
初期のFORTRANではセミコロンは使われていません。基本的には1行一文です。
FORTRANのプログラムはパンチカードにパンチして計算機に入力していました。
By ArnoldReinhold - Own work, CC BY-SA 3.0, File:Punched card program deck.agr.jpg - Wikimedia Commons
これが一つのプログラムでカード一枚がFORTRANの一文です。(注:写真の最初の一枚はFORTRANではな
Cのセミコロンは文の終わりの記号ですが、せっかくなのでプログラミングでのセミコロンの歴史に関して以下の記事を参考にしながら考えてみたいと思います。
英語でのセミコロンの意味ですが、コンマ(,)より強くピリオド(.)より弱い文の区切りだそうです。
上のサイトで例文として
I went to the library; Jonathan went to the theater.
「私は図書館に行き、ジョナサンは劇場に行った」
が挙げられていてセミコロンの前と後ろは独立の文で、しかも関連が強い場合に使うようです。
さてプログラミングの話に進めましょう。
世界で初めての高級言語はFORTRANと言われています。
初期のFORTRANではセミコロンは使われていません。基本的には1行一文です。
FORTRANのプログラムはパンチカードにパンチして計算機に入力していました。
By ArnoldReinhold - Own work, CC BY-SA 3.0, File:Punched card program deck.agr.jpg - Wikimedia Commons
これが一つのプログラムでカード一枚がFORTRANの一文です。(注:写真の最初の一枚はFORTRANではなくJCLとよばれるもので、この一群のカードはFORTRANのプログラムですとOSに伝えるためのものと思われます。)
カード一枚で一文なので文(ステートメント)の終わりと特別な記号で表す必要はありません。
カード一枚で収まりきれない文をどうするかという疑問が湧くと思いますが、そのときは「継続行」仕組みで対処します。
実際のプログラミン作業ですが、カードにパンチしたプログラムを連続用紙に印字してもらって机上デバッグをします;机の上のプリントアウトを見て間違いを見つける作業です。下の写真はまさにFORTRANプログラムのプリントアウトです。
実際のFORTRANプログラムのコードはこんなものです。
この方式はIBMの80桁のカードを前提とした仕様です。
まず作ってしまうアメリカに対して、理論的な基盤、規格にこだわるヨーロッパを中心に合理的な言語の規格として提案されたのがALGOLという言語です。
ALGOLは入力機構とは独立にプログラムを記述できる言語を目指して規格を作りました。当然言語としての書法はかなり英語の影響を受けています。紙に英文を書くようにプログラムを記述できるということです。
1行一文方式だと1行の情報量が少なく、1行に複数の文を記述することを想定し、最初の英文の例のセミコロンを着想したのではないでしょうか。
1960年のALGOL60という規格で文の区切りとしてセミコロンが導入されました。ALGOLは、FORTRANとは異なり文法理論に則った構文となっています。コンピュータサイエンスを学ばれた方はBNF(バッカス・ナウア記法)という文法を記述する記法をご存知だと思います。BNFはALGOLの構文を記述するために発案されたものです。このバッカスはFORTRANの開発者のジョン・バッカスのことです。彼もALGOLの委員でありヨーロッパ中心といってもアメリカを含めた当時の一流の研究者、技術者が策定した規格です。
ALGOL60のその後の手続き型言語とコンピュータサイエンスに多大な影響を与えています。
以下がALGOL60のコードの一部分です。
- FOR i := 0 STEP 1 UNTIL 999 DO
- BEGIN
- IF candidates[i] # 0 THEN
- BEGIN
- write(1,i);
- text(1," is prime*N")
- END
- END;
5行目にセミコロンが使われていてwrite文とtext文の区切りとして使われています。
余談になりますが私の大学院時代の指導教官の清水留三郎先生は日本で初めてALGOLのコンパイラをインプリメントした人です。
1960年代にセミコロンを採用した言語にIBMのPL/Iがあります。PL/IはFORTRAN,COBOL,ALGOLの機能を包含しようとした野心的なプログラミング言語です。
PL/Iのコードの一部を示します。
- DO I = 1 TO LENGTH(INPUT_TEXT);
- HUO0 = SUBSTR(INPUT_TEXT,I,1);
- IF HUO0 = ' ' THEN DO;
- HUO1 = ' ';
- END;
- ELSE DO;
- HUO1 = ASCII_TO_CHAR((CHAR_TO_ASCII(HUO0) + ENCRYPT_KEY));
- END;
- SUBSTR(OUTPUT_TEXT,I,1) = HUO1;
- /*PUT SKIP LIST('I = ' || I);*/
- END;
この例で注目してもらいたいのはENDの前の文です。ENDの前の文にも全てセミコロンがついています。ALGOLは文の「区切り」としてセミコロンを使っているためENDの前の文にはセミコロンがついていません。PL/Iではセミコロンを文の終端の必須のマークとして使っています。
ここで改行を終端としないメリットを考えてみたいとおみます。
PL/Iの例を一部を編集してみました。
- ELSE DO;
- HUO1 =
- ASCII_TO_CHAR(
- (
- CHAR_TO_ASCII(HUO0)
- +
- ENCRYPT_KEY
- )
- );
- END;
一つの文を改行して読みやすくすることができます(好みの問題もありますが)。
C言語(1972年)はALGOLとPL/Iから影響を受けていてセミコロンを文の終端としています。
C言語が文の区切りではなく文の終端としてセミコロンを使ったのはコンパクトなコンパイラーをつくるためです。
ALGOLのBEGIN, ENDを { }にしてしまったのもC言語です。
Cの仕様とコンパイラがシンプルであったこと、さらに米国の独禁法の関係からUnixに含まれるかたちでCのソースコードが無償で公開されたことによりCが現在のITに多大な影響を与えます。
C言語とUnixは多くの大学や研究所のマシンに移植され活発にさまざまなソフトウェアがCで開発されました。インターネットのプロトコルスタックのほとんどはCで記述されています。プログラミング言語を開発するための様々のツールもCで開発されています。最初のWeb技術は、C言語にオブジェクト機能を追加したObjective Cで開発されています。
この有名なプログラムもC言語とともに広がりました。
- #include <stdio.h>
- main( )
- {
- printf("hello, world\n");
- }
C言語はQuoraをはじめ皆さんがよく見るスマフォのアプリやWebのアプリを作るためには最適とは言えません。現在、Pythonをはじめ様々言語やフレームワークが利用できますが、それらはC言語で記述されたさまざまなソフトウェア資産(人材も含む)の上に作られていることを忘れないで欲しいと思います。
詳細な歴史をものすごく簡略化して言うと、C言語は、UNIXオペレーティングシステムをアセンブリ言語で書く作業が人間にはつらすぎるという問題を解決するために、アセンブリ言語を置き換える最良の言語として作られました。また、アセンブリ言語は、マシン語(0と1のビット列)を直接入力する作業が人間にはつらすぎるという問題を解決するために、マシン語を置き換える最良の言語として作られました。
つまり置き換えは、マシン語>アセンブリ言語>いくつかの言語>C言語 という感じで進んできました。C言語も完全ではないため、C言語の次の置き換え候補も、当然、さまざまな人が考案しています。
C言語の問題点を解決しようとする言語は、List of C-family programming languages - Wikipedia を参照するまでもなく、たくさん派生しています。C++,C#,Java,Go,JavaScript,Rust,Ruby,PHP,Python,Perl など、現在広く使われている多くの言語が、C言語の派生言語です。このリンク先には、Go言語がないですね、驚きです!しかしGo言語を作った人たちは、Go言語はC言語の問題を解決する言語だと主張しています。
さて、C言語から派生したこれらの言語のなかで、JavaやJavaScriptやC#,PHP,Python,Rubyなどの言語は、C言語の機能の
詳細な歴史をものすごく簡略化して言うと、C言語は、UNIXオペレーティングシステムをアセンブリ言語で書く作業が人間にはつらすぎるという問題を解決するために、アセンブリ言語を置き換える最良の言語として作られました。また、アセンブリ言語は、マシン語(0と1のビット列)を直接入力する作業が人間にはつらすぎるという問題を解決するために、マシン語を置き換える最良の言語として作られました。
つまり置き換えは、マシン語>アセンブリ言語>いくつかの言語>C言語 という感じで進んできました。C言語も完全ではないため、C言語の次の置き換え候補も、当然、さまざまな人が考案しています。
C言語の問題点を解決しようとする言語は、List of C-family programming languages - Wikipedia を参照するまでもなく、たくさん派生しています。C++,C#,Java,Go,JavaScript,Rust,Ruby,PHP,Python,Perl など、現在広く使われている多くの言語が、C言語の派生言語です。このリンク先には、Go言語がないですね、驚きです!しかしGo言語を作った人たちは、Go言語はC言語の問題を解決する言語だと主張しています。
さて、C言語から派生したこれらの言語のなかで、JavaやJavaScriptやC#,PHP,Python,Rubyなどの言語は、C言語の機能のうち、システムプログラミングのための機能、つまり指定したメモリアドレスに直接アクセスしたり、マシン語を直接生成して実行したりする機能を捨ててできなくすることで、生産性と安全性を高めました。そのため当然ですが、ハードウェアを直接操作するプログラム(OSなど)を作りたい場合には、こうした言語は不向きです。しかし反対に、GUIアプリケーションやゲーム、Webサーバなどを作るときには、C言語よりも少ないコード量で、安全なプログラムを早く作ることができます。現在ではアプリケーションの用途は多岐に渡るので、用途ごとに特有の課題をうまく解決する言語が乱立するようになりました。
C言語以外でシステムプログラミングが全くできないということはありません。C言語から派生した言語の多くが、C言語で書かれた外部モジュールを読み込んで使えるように実装されています。C言語のシステムプログラミングの能力をこうした外部モジュールを経由して利用することができます。ただし、オペレーティングシステムのように、プログラムほとんど全体でシステムプログラミングが必要であるようなプログラムの場合は、外部モジュールを用いた実装方法は明らかに向いていません。
現在、オペレーティングシステム(OS)を実装することができるC言語の派生言語で有力なものは、Go言語とRust言語です。特にGo言語が突出しています。
Go言語を用いてOSを作ろうというプロジェクトはいくつかありますが、ソースが小さくて読みやすいものを紹介します:
Go言語はアセンブリ言語で書かれたモジュールを読めるようになっているので、OSのブートストラップのところだけアセンブリで書かれていますが、それ以外はすべてGoで書かれています。ブートストラップのところは厳密に正確なマシン語が必要であるため、C言語で書かれたOSであっても、Cからアセンブリ言語のモジュールを読むように実装されています。
また、ls,cp,rm,ln,cat,shなど、UNIXの重要な要素である、基本ツール群もCで書かれていますが、それらすべてをGoで書き直すプロジェクトがあります。ソースがC版とどう違うのかを見ると楽しめるでしょう。
「偉い」とか great と言うのが、そもそも「どういう事」なのか、よく分かりませんが、
よく分からないなりに言うと(汗)、
「かなり偉い」
と私は思います。
なぜなら、現在よく使われているプログラミング言語のほとんど(※1)、および OS のカーネルは(※2)、C で書かれているからです。
もし C が無かったとしたら、それらの多くは、今なおアセンブリー言語で書かれていたでしょう。
あるいは C に代わり得る別の言語がいずれ出現したでしょうが、C が 1972年に出回ってから、Rust が出るまで、50年近く掛かってしまった事を考えると、やはり C は偉大な発明だったと思います。
C の「偉さ」に比肩し得るのは、C で作られなかった言語達、つまりアセンブリー言語、FORTRAN、COBOL、PL/I、ALGOL、Pascal、あたりではないでしょうか。 彼らにとって「偉い」のは機械語であり、アセンブリー言語ということになるでしょう(※3)。
しかし彼らは、C ほど多くのモノを作り出すには至りませんでした。 私の私的な感覚ですが、C 以前と C 以降では——こういう言い方が適切なのかどうかよく分からないのですが——、プログラミングに対するプログラマーの取り組みと言うか姿勢と言うか哲学と言うようなものが、大きく変わった、という感じを持っています。
これは UNIX OS の影響が大きかったという事も
「偉い」とか great と言うのが、そもそも「どういう事」なのか、よく分かりませんが、
よく分からないなりに言うと(汗)、
「かなり偉い」
と私は思います。
なぜなら、現在よく使われているプログラミング言語のほとんど(※1)、および OS のカーネルは(※2)、C で書かれているからです。
もし C が無かったとしたら、それらの多くは、今なおアセンブリー言語で書かれていたでしょう。
あるいは C に代わり得る別の言語がいずれ出現したでしょうが、C が 1972年に出回ってから、Rust が出るまで、50年近く掛かってしまった事を考えると、やはり C は偉大な発明だったと思います。
C の「偉さ」に比肩し得るのは、C で作られなかった言語達、つまりアセンブリー言語、FORTRAN、COBOL、PL/I、ALGOL、Pascal、あたりではないでしょうか。 彼らにとって「偉い」のは機械語であり、アセンブリー言語ということになるでしょう(※3)。
しかし彼らは、C ほど多くのモノを作り出すには至りませんでした。 私の私的な感覚ですが、C 以前と C 以降では——こういう言い方が適切なのかどうかよく分からないのですが——、プログラミングに対するプログラマーの取り組みと言うか姿勢と言うか哲学と言うようなものが、大きく変わった、という感じを持っています。
これは UNIX OS の影響が大きかったという事も有るかと思います。 インターネットも UNIX が無ければ発達しなかったでしょう、つまり C が無ければ……(C は UNIX を書くために作られた)。
ところで、こうした「低層な観点」とは別に、やたらと偉い言語が1つあります。
LISP です。
先述した、「プログラミングに対するプログラマーの哲学」に、LISP は C に劣らず強い衝撃と影響をもたらした、と私は思います。 ※1で引用した記事における「不動の第1位」は JavaScript ですが、LISP 無くして JavaScript は生まれなかったでしょう。
JavaScript が今やどれほど偉いのか、それは説明を要しますまい。 LISP の血を受け継ぎ、C/C++ で書かれた JavaScript が、「不動の第1位」である事を考えると、C と LISP の偉さが、なんとなく分かるような気がします。
※1 正直言って、私はこのリンク先記事で言及されている 20言語のソースコードを見た事は無いのだが、これらの9割が C か C++ で書かれているだろう事には千円くらい賭けてもよい。
※2 「OSの開発にC++よりもCが使われていることが多い理由は何ですか?」における Kurimoto Shingo 様回答に付けられた Takahashi Takahashi 様コメントに、C が使われる理由が簡潔に書かれている。
※3 Pascal コンパイラーを Pascal で書いた実例が有るそうなので、Pascal はこれらの中ではやや別格——つまり「少し偉い」——かも知れない。 FORTRAN や COBOL で自言語コンパイラーが作れないとは言えないが、面倒過ぎて挑戦者が現れないであろう。
C言語のポインタを学んだ頃に抱いた疑問・誤解と克服
1. C言語規格の意味がコンパイラを書いてみるまでわからなかった。コンパイラを書いてみるとC言語規格の未定義、未規定、処理系定義が、プログラマの精神に基づき、規格がないCPUの発展を妨げない範囲で、自由にプログラムが書けるようにするものであることがわかった。
http://www.open-std.org/jtc1/sc22/wg14/www/docs/C99RationaleV5.10.pdf
- Trust the programmer.
- Don’t prevent the programmer from doing what needs to be done.
- Keep the language small and simple.
- Provide only one way to do one operation
- Make it fast, even if it is not guaranteed to be portable.
2. 自分の書いたコンパイラが、メモリ管理がずさんで、デバッグモード以外は暴走する。静的検査、MISRA Cなどの規則に基づいて検査するとかなりメモリ周りの不具合が取れそう。
3. CPUが16bitから32bitになってメモリの管理方法が複雑になり、わけがわからなくなった。複数のOSを同時に利用できるようになり便利になっ
C言語のポインタを学んだ頃に抱いた疑問・誤解と克服
1. C言語規格の意味がコンパイラを書いてみるまでわからなかった。コンパイラを書いてみるとC言語規格の未定義、未規定、処理系定義が、プログラマの精神に基づき、規格がないCPUの発展を妨げない範囲で、自由にプログラムが書けるようにするものであることがわかった。
http://www.open-std.org/jtc1/sc22/wg14/www/docs/C99RationaleV5.10.pdf
- Trust the programmer.
- Don’t prevent the programmer from doing what needs to be done.
- Keep the language small and simple.
- Provide only one way to do one operation
- Make it fast, even if it is not guaranteed to be portable.
2. 自分の書いたコンパイラが、メモリ管理がずさんで、デバッグモード以外は暴走する。静的検査、MISRA Cなどの規則に基づいて検査するとかなりメモリ周りの不具合が取れそう。
3. CPUが16bitから32bitになってメモリの管理方法が複雑になり、わけがわからなくなった。複数のOSを同時に利用できるようになり便利になったので原理は理解できていなくても大丈夫。
まとめ
C言語のポインタはアセンブラを書けばわかる。知識は不要。アセンブラで書かない処理をC言語のポインタで書くのは、無理筋。ポインタがわからないのではなく、書こうとしている処理がわかっていない。
CコンパイラかOSを書いてみれば、いかにCPUを効率的に使うための言語であり、CPUまわりの記述をするための道具であることがわかる。アプリを書くための道具ではない。
C言語のポインタが難しいのではない。難しいことをポインタで書こうとするのが間違いだと気がつけばよい。
C言語のポインタなど文法から学ぼうとするのが無理で無駄。コンパイラ、OSなどのC言語で書かれているものから学べばよい。
自分のブログ URL を1つ書けば済むのですが、Quora では「宣伝や商業目的でQuoraユーザーたちを外部サイトへ誘導」する回答はスパムと見なされるので、そのブログ内容を自分で(ここの回答向けに適宜編集して)再掲します。
1.はじめに
C のポインタが「難しい」と言われる理由はいくつかありましょうが、大まかに言って次の3つの理由からだろうと私は考えています。
(1)C におけるポインタの文法が変な書き方で分かりにくい。
(2)分かりやすく書かれた書籍が非常に少ない。
(3)他の言語ではポインタが使われないように見えるので、何のために使うのかが分かりにくい。
本回答では(3)に重点を置いて説明します。 誤解が無いように書くと長文になってしまうのですが、ゆっくり読めば、全く難しくありません。 必ず分かるという自信を持って、あせらず、ゆっくり読んでみてください。
2.ポインタとは、そもそも何者なのか
ポインタ(pointer)という言葉は point-er ですから、「指し示す者」と訳せます。 猟犬にポインターという犬種がいますが、あれは、仕留めた獲物の場所を教えてくれるので、そういう名前になったのです。
C のポインタも、それに似ています。 C のポインタとは、何かを指し示すための型、変数です。
一体、何を指し示すのか……それは、「メモリ(memory)に格納されている何か」です。
「何か」は、in
自分のブログ URL を1つ書けば済むのですが、Quora では「宣伝や商業目的でQuoraユーザーたちを外部サイトへ誘導」する回答はスパムと見なされるので、そのブログ内容を自分で(ここの回答向けに適宜編集して)再掲します。
1.はじめに
C のポインタが「難しい」と言われる理由はいくつかありましょうが、大まかに言って次の3つの理由からだろうと私は考えています。
(1)C におけるポインタの文法が変な書き方で分かりにくい。
(2)分かりやすく書かれた書籍が非常に少ない。
(3)他の言語ではポインタが使われないように見えるので、何のために使うのかが分かりにくい。
本回答では(3)に重点を置いて説明します。 誤解が無いように書くと長文になってしまうのですが、ゆっくり読めば、全く難しくありません。 必ず分かるという自信を持って、あせらず、ゆっくり読んでみてください。
2.ポインタとは、そもそも何者なのか
ポインタ(pointer)という言葉は point-er ですから、「指し示す者」と訳せます。 猟犬にポインターという犬種がいますが、あれは、仕留めた獲物の場所を教えてくれるので、そういう名前になったのです。
C のポインタも、それに似ています。 C のポインタとは、何かを指し示すための型、変数です。
一体、何を指し示すのか……それは、「メモリ(memory)に格納されている何か」です。
「何か」は、int型変数の場合もありますし、char型変数だったり、配列だったり、あるいは何らかの構造体だったりします。 場合によっては関数だったりします。 これら「メモリに格納されている何か」達を、総称して「オブジェクト(object)」とも呼びます。
関数は実行出来るオブジェクト、定数やリテラル(即値)は参照しか出来ないオブジェクト、int型変数は整数を格納するオブジェクト、float型変数は浮動小数点数を格納するオブジェクト、ポインタ変数は「オブジェクトを指し示すオブジェクト」、なのです。
では、C で扱うオブジェクト(変数・定数・即値・関数)が置かれている「メモリ」とはどういうモノか……それは「同じ大きさの小箱を、たくさん一列に並べた」ようなもので、それぞれの小箱にアドレス(address)という整数値の通し番号が付けられています。 C プログラム上においては、この小箱1つのサイズは1バイトであり、それは char型サイズと同じである事が、C の規格で決まっています(実際のコンピュータのメモリが、たとえ1ワード14ビットであろうが関係なく、C のメモリの小箱1つは1バイトです)。
あるオブジェクトが、小箱いくつ分のサイズなのか(何バイトなのか)は、そのオブジェクトの型(type)によって決まります。 float型なら4つで char型なら1つ、などです(char型以外の型サイズは、処理系依存です)。
1つの型は、メモリ上で連続した小箱に置かれます。 float型が、2つの小箱と、別のところにある2つの小箱に分断される事はありません。 また、配列は全要素が連続した小箱に置かれます。 従って、オブジェクトの先頭アドレスを使えば――それがどんなに大きなサイズであっても先頭アドレスだけで――、それを指し示す事が出来ます。
(オブジェクトが、複数のオブジェクトの組み合わせから成る場合、それらがメモリのあちこちに分散する事は普通にありますが、「最初の・先頭オブジェクト」の型1つは、やはり連続した小箱に置かれます)
ポインタは、対象が置かれているメモリ領域の「先頭アドレス値と型を格納する」ことによって、対象オブジェクトを指し示します。 アドレス値と型があれば、対象のオブジェクトがメモリの「どこにあり」「どこまであるのか」が分かるからです。
たとえて言えば、ポインタとは、「住所を書き留めるための小さな紙きれ」のようなものです。 「東京都千代田区1-1」という住所を、小さな紙切れに書いて机上に置いておいたり、コピーしたり、誰かに渡す事は簡単に出来ますが、「皇居そのもの」を机上に置いたりコピーしたり誰かに渡すなんて事は、まず不可能です。
「皇居オブジェクト」そのものを扱うのは大変ですが、「皇居の住所オブジェクト」は、簡単に扱える……住所(address)という概念は、素晴らしいと思いませんか?
なお、C のアドレス値は整数なのですが、ポインタは整数型ではありません。 ポインタは対象オブジェクトの型情報も持つからです。 また、ポインタ変数が持つアドレス値の具体的な値は、特殊な場合を除き、気にする必要はありません。
(ポインタがどういう仕組みなのかは処理系依存であり、printf( ) 関数の出力変換書式 %p でアドレス値を表示する際の出力形式も、処理系依存です)
3.基本的な文法
ここでは、C のポインタにまつわる、基本的な3つの書き方、「宣言」、「アドレス取得」、「参照」について触れます。
3.1 ポインタ型変数の宣言
ポインタ型変数を宣言するには、次のように書きます。
- int * a; /* 「int型領域を指すポインタ型変数」 a を宣言 */
- char * b = "ABC"; /* 「char型領域を指すポインタ型変数」 b を宣言 */
型名の後ろにポインタ宣言子の * が有る変数宣言は、「ポインタ型変数」宣言です。 指定した型の領域のアドレス値に限り格納できる変数が、この宣言後に使えるようになります。 ポインタ型変数は、単にポインタとも呼びます。
上記例のポインタ a は、int型領域しか指し示すことが出来ません。 他の型の領域のアドレスを設定すると、コンパイル・エラーになります。 ポインタは型情報を持つからです。
(なお、文字列の初期化には特別な決まりがありますが、ここでは割愛します)
「皇居のたとえ」で言えば、住所をメモするための紙切れを用意したり、すでに住所が書かれている紙切れを用意する、というのが、この「ポインタ型変数の宣言」です。
型に void を指定すると、「特に決まった型を指さないポインタ」が宣言されます。 void * 型ポインタは、キャスト(型を強制的に読み替えること、あまり推奨はしません)によって、様々な型の領域を指し示す事が出来ます。
- void * c; /* 「指す領域の型を特定しないポインタ」 c を宣言 */
3.2 アドレス取得
単項アドレス演算子 & を使うと、その対象オブジェクトが占有しているメモリ領域の先頭アドレス値が得られます。 「単項」というのは、マイナス符号のように、「相手が1つだけ」という意味です(= とか / は、2つの相手が要るので、2項演算子です)。 以下のように使います。
- int d = 365; /* 「int型の領域を占有し整数を格納する変数」 d を宣言 */
- a = &d; /* a には d の先頭アドレス値が入る */
上記例で、&d は、変数 d のアドレス値、すなわち d の「住所」を示しています。 変数 a は int型専用ポインタですから、int型のアドレスである &d が代入出来ます。
「皇居のたとえ」で言えば、皇居の住所を調べるのが、アドレス演算子の役割だ、と言えます。
3.3 参照
ポインタに格納されたアドレス値そのものを見ても、そのアドレスに格納されているモノ自体については全く分かりません。 「東京都港区赤坂2-3」という住所だけ見ても、そこに何があるか、何が建っているか、誰がいるかは分からないのと似ています。
何らかのアドレス値を格納しているポインタに、単項参照演算子 * を付けると、対象としているアドレスの内容値、つまりメモリの内容が得られます。 この参照演算子 * は、ポインタ宣言子の * とは、全く意味が違う「別物」です(これは混乱を招く文法だと思います……)。 以下のように使います。
- printf( "%d\n", *a ); /* 参照した内容値 365 が表示される */
- *a = 123; /* 参照した内容値が書き換えられる */
- printf( "%d\n", d ); /* 書き換えられたので 123 が表示される */
「参照」とは、「皇居のたとえ」で言えば、「東京都千代田区1-1」と書かれた紙片を見て、その住所へ実際に出かけてみるようなものです。 変な・架空の住所が書かれていたり、白紙だと困ってしまいます。 * で参照されるポインタには、あらかじめ何らかのアドレス値が入っていなければなりません。
上記の例で示した「参照先の書き換え」は、たとえて言えば「住所が書かれた紙片を見て、その住所にある建物を建て替える」ようなものです。
4.局面その1「そのモノ自体を持ち回りたくない!」
ここからは本回答の本題、ポインタの「使いどころ」について説明します。
「メモリ上にある int や char や struct や配列は、その変数名で直に示せるじゃないか、わざわざポインタで示さなくても」……と思いませんか。
ポインタで書けるプログラムは、配列でも書けたりしますし、配列の方が分かりやすい感じがします。 一体、どんな時にポインタを使いたくなるのでしょうか。
関数の外側で宣言した変数は、どこからでも変数名でアクセス出来ますので、ポインタの出番は無さそうです。 1つの関数の中だけで使う変数も、同様に、ポインタの出番は無さそうです(アルゴリズムの都合でポインタにしたい場合はあるかも知れませんが)。
……ということは、ポインタが役に立ちそうなのは、「関数の中で宣言した変数・領域を、別の関数で使いたい時ではないか」、と予想出来るのではないでしょうか。
呼び出し元の関数が持つ値や、宣言した変数(の内容値)を、別の関数で使いたい時は、引数として渡す事が出来ます。
引数のある関数を呼び出す時、C では呼ぶ側の実引数の内容値が、呼ばれ側の関数の仮引数にコピーされます。 int型の引数なら4ないし8バイト、char型の引数なら 1バイト、必ずコピー動作が入るのです。
実用的なプログラムでは、そこそこ大きな構造体や配列を、関数の間で持ち回りたい、という事が、よくあります。
しかし、関数の引数に構造体や配列を書くと、関数を呼び出す度に、大量のコピー動作が発生してしまいます。 1回や2回程度なら許容範囲かも知れませんが、ループの中で関数呼び出しが有ったりすると、処理時間がそれだけ掛かります。
そこで、呼ぶ側で「持ち回りたい領域」の先頭アドレスを用意して、これを引数として関数を呼ぶ、という事が考え出されました。 アドレス値を渡すので、受け取る側の関数は、仮引数をポインタ型にしなければなりません。
呼ばれ側で、ポインタに * を付けて参照すれば、目的の領域を扱う事が出来ます。
ポインタのサイズは、多くの場合、4~8バイトに過ぎませんが、ポインタ1つで、数百、数千、数メガバイト、それ以上の領域を「持ち回る」事が出来ます。
「大きな領域を持ち回る」関数呼び出しが「何回も行われる」場合、ポインタを使う場合と、使わない場合とでは、処理速度が大きく違ってくる……これは、「ポインタを使うと速い」と言われる理由の1つです。
5.局面その2「そのモノ自体を動かしたくない!」
int a[ 100 ]; という配列があったとして、その内容を昇順に並べ替える(ソート(sort)、ソーティング(sorting))プログラムというものを、C を学ぶ人は、1度は見たり作ったりする事でしょう。
ソートでは、配列要素を比較する動作と、入れ替える動作が必要になりますが、要素の型が int であれば、入れ替えは単純に代入演算子 = で済みます。
では、これが int a[ 100 ] ではなくて、下記のようだったら、どうやって入れ替えれば良いでしょうか。
- struct mydata { /* 構造体 mydata */
- char c; /* メンバ c */
- int n; /* メンバ n */
- float f[ 100 ]; /* メンバ f[ ] */
- };
- struct mydata a[ 100 ]; /* これがソート対象 */
「代入先 = 代入元 とせずに、memcpy( 代入先, 代入元, sizeof( struct mydata ) ) で入れ替える」。
……正解です。 正攻法です。
しかし、もし struct mydata が巨大であれば、正攻法では処理時間が長くなってしまいます。 また、処理時間の見積もりも、struct mydata の大きさに依存して変わってきます。
こういう場合にも、ポインタの出番です。
まずは、struct mydata a[ 100 ] の他に、ポインタの配列 struct mydata * ap[ 100 ] を用意します。 そして、ソートする前に、あらかじめ ap の各要素に a の各要素の先頭アドレスを設定しておきます。
- int i; /* あらかじめ宣言しておく */
- ~
- /* ソートの前準備 */
- for ( i = 0; i < 100; i ++ ) {
- ap[ i ] = &a[ i ]; /* a[ i ] の先頭アドレスが ap[ i ] に格納される */
- }
比較は、a[ ~ ] 同士を直で比較しても良いですし、*ap[ ~ ] で a[ ~ ] の内容値を参照して行っても構いません。
そして「入れ替え」は、memcpy( ) を使わず、下記のように行います。
- struct mydata * workp; /* あらかじめ宣言しておく */
- ~
- /* 入れ替え開始 */
- workp = ap[ 入替先 ];
- ap[ 入替先 ] = ap[ 入替元 ];
- ap[ 入替元 ] = workp;
- /* 入れ替え終了 */
この場合、入れ替え1回で発生する代入動作は、workp への代入、ap 同士の代入、workp からの代入、この3つです(32ビットシステムでは、大抵 12バイト分のコピーに過ぎません)。 そして、対象の構造体が、どんなに大きくても、このコピー量は一定です(ここが大事です)。
ソートが終わって結果を表示する時には、ap[ ~ ] に * を付けて「参照」したものを使います。
- for ( i = 0; i < 100; i ++ ) {
- printf( "%d\n", ( *ap[ i ] ).n ); /* メンバ n を表示してみる */
- }
これは、「指し示すもの(ポインタ)」だけを入れ替えて、「指し示される対象(実データ)」は全く動かさないという、C では定番のテクニックです。 こうすれば、対象データの量にかかわらず、入れ替えの処理速度が一定かつ高速になります。 これも、「ポインタを使うと速い」と言われる理由の 1つです。
ちなみに、上記の printf( ) は printf( "%d\n", ap[ i ] -> n ); とも書けます。 -> はアロー演算子と呼ばれ、左辺がポインタの時のみ使える、構造体メンバを指定するための演算子です。
α->β は ( *α ).β と等価であり、構文糖(syntax sugar)に過ぎませんが、アロー演算子を使うと、左辺がポインタである事を明示出来ます。 以下のように使い分けられています。
α->β … αはポインタ、βはメンバ
α.β … αは構造体の実体、βはメンバ
6.局面その3「複数の値を関数の引数でやりとりしたい!」
先ほど書いた通り、C では、引数付きの関数を呼び出す際、実引数(呼ぶ側の値)から仮引数(呼ばれ側の変数)へ値をコピーします。 つまり、引数の値のやりとりは、「呼び出し元→呼び出し先」の一方通行しかありません。
呼び出し先の関数から返してもらいたい値が1個だけなら return 文で返せますが、複数個を返したい場合、これでは困ってしまいます。
ここでも、ポインタの出番となります。
複数の値を関数間でやりとりしたい、しかもそれらを引数にしたい、という場合は、呼ばれ側の関数の仮引数をポインタ型にしておきます。
呼ぶ側では、やりとりしたい対象のアドレスを実引数として渡します。 そのアドレス値は、呼ばれ側の仮引数――ポインタ型です――にコピーされます。
呼び出された側の関数では、何らかの結果を、その仮引数変数(ポインタですのでアドレス値が入っています)を「参照」した場所に設定します( * 演算子を使う)。
この「参照」した場所とは、もちろん、呼ぶ側の関数で用意された「対象」が置かれているメモリ領域です。
こうすれば、戻り値が2つあろうが3つあろうが、いくらでも、好きな個数だけ、やりとりする事が出来ます。
時系列で書くと、以下のような動作になります。
・呼ぶ側で、結果の欲しい対象領域を用意する(いくつでも良い)。
・そのアドレスを取得して実引数とし、目的の関数を呼び出す。
・そのアドレスが、呼ばれ側の関数の仮引数にコピーされる。
・呼ばれ側の関数では、仮引数を * で参照したところに結果を設定する。
(まさにこの時、呼び出し元の関数の「結果の欲しい対象領域」の内容が書き換わる)
・呼ばれ側の関数が終了し、呼び出し元の関数に戻る。
・すでに「欲しい結果値」は得られている。
これも C でよく使われる定番テクニックですが、呼ぶ側で構造体を用意しておいて、そのポインタを引数として渡す、というテクニックもしばしば使われます。 構造体の中には多くの内容を詰め込めるので、実用的なテクニックと言えます(実装は局面その1「そのモノ自体を持ち回りたくない!」と同様ですが、目的が違うわけです)。 このテクニックをさらに突き詰めて発展させると、クラスベースのオブジェクト指向に近いものになります。
7.応用局面「大きさがよく分からないモノを扱いたい!」
「何らかの実体を扱うにおいて、その実体に触れず、その先頭アドレスを扱う」ポインタ……その重要な「使いどころ」の1つが、これです。
たとえば、ネットワーク通信で、外部からいくつものパケットを受信するプログラムを作る、とします。 そのパケットは、固定長のヘッダと、可変長の内容から成っていて、ヘッダには内容の長さが含まれているとしましょう。 また、通信相手は、同時に最大 1000カ所まではさばける仕様である、とします。
パケット内容長は、0 だったり 1 だったりする事もあれば、100 だったり1万だったりする事もあります。 こういう場合、内容を格納しておくための領域は、どう宣言すれば良いでしょうか。
- char packet_content[ 1000 ][ 10000 ];
……泥臭い実装ですが、これは正しい解です。 1万バイト以上のパケットについては、ヘッダ内の内容長をチェックして受け付けないようにすれば、危険もありません。 しかもこのコードは、一般論的に言って高速です。
しかし、この配列定義は 10MB のメモリを消費します。 パケットの多くが数百バイトだとしたら、これは壮大な無駄です。 また、「同時に最大 1000カ所」とか、「最大1万バイト」という条件が、将来的に変わったら、無駄は、さらに増えるでしょう。
メモリを無駄に使っても、簡単・高速に動かしたい、という場合は、これでも良いのですが、無駄をなるべく省きたい場合は、ポインタの出番です。
まずは、次のように宣言します。
- char * packet_content[ 1000 ];
これは、char型領域を指すポインタを 1000個並べただけで、パケット内容を格納する領域はどこにもありません。 とりあえず「メモ用紙の束」だけ作っておいた、という感じです。 この配列定義が消費するメモリは、たかだか数KB です。
パケットを 1つ受信したら、そのヘッダ内の内容長を見て、次のようにします。
- packet_content[ パケット番号 ] = malloc( 内容長 );
malloc( ) は、「指定されたバイト長の連続したメモリの小箱を確保して(OS から借りる)、その先頭アドレスを返してくれる」関数です。 stdlib.h の中にあります。 こういう動作を、「動的メモリ確保(dynamic memory allocation)」などと言います。
こうすれば、パケット毎に、異なる長さのメモリを、弾力的に確保出来るので、無駄を非常に少なく出来ます。
確保された領域の(0から数えて)3バイト目にアクセスするには、次のように書きます。
- *( packet_content[ パケット番号 ] + 3 ) = 'A'; /* 'A' を書き込んでみる */
下記のように書いても同じです。
- packet_content[ パケット番号 ][ 3 ] = 'A';
まともに [ 10000 ] と確保するのに比べて、malloc( ) するのは実行時間もかかりますし、プログラムも若干複雑になりますので、「実行時間コストとメモリ消費コストはバーター(引き換え)である」と言えます。
メモリコストを重視する場合は、実行時間コストが多少かかっても、動的(dynamic)な処理を選ぶ事になりますが、その場合、ポインタは欠かせません。
8.他の言語では……
ポインタの存在が見えない他言語においても、「実体を動かさず『参照』する」のは、実行時間やメモリを大きく節約できる、魅力的な機能のはずです。
実際、C より新しい言語は、ほとんどが「ポインタを実装」しています。 ただ、文法的に、それを表に出さないようにしているだけです。
たとえば Java では、必要なオブジェクトを new で作り出しますが、作り出されたオブジェクトは、内部的には「ポインタで持ち回る」形で扱われています。
- JPanel myPanel; // myPanel の正体は C で言うところのポインタ。C っぽく書くと、struct JPanel * myPanel;
- myPanel = new JPanel( ); // new は「新しく割り当てた領域の先頭アドレス」を返す。C の malloc( ) にプラスアルファしたものと考えてよい。
- myPanel.setLayout( ~ ); // C っぽく書くと、(*myPanel).setLayout( ~ );
他の言語でも、クラスや構造体、配列など、「ある程度大きなモノ」に付けられた「名前」は、内部的にはポインタである、という事は、よくあります。
こういう言語では、「わざわざ * を付けて、参照である事を明示」しなくても、名前を書くだけで「参照になる」のですが、その代わり、ポインタそのものを「ポインタとして扱う」ことは出来ないわけです。 C は、その辺りを省略せず、細かく書く言語なのです。
9.おわりに
C は、それなりに古く、今から見ると非力なコンピュータのために作られた言語です。 C が生まれた頃、複雑かつ実用的な速度で動くプログラム(たとえば OS)は、アセンブリ言語で作られていました。
そうしたプログラムを、なんとか高級言語で書きたい、生産性の良い、読みやすいプログラムを作りたい、という欲求が有ったのは当然でした。 しかし当時のコンピュータの性能で、「アセンブリ言語に比肩し得るプログラムを書ける高級言語」を考えれば、ポインタを実装せざるを得なかったのでしょう。
ポインタはアセンブリ言語っぽい概念なので、C は「高級アセンブラ」と揶揄される事もありますが、他の言語にはない、独特のテイストやバランスを生み出してもいます。 そのテイスト、バランスは、結果論として得られたものかも知れませんが、そのテイストとバランスゆえに、C は多くのプログラマに愛用され、今も生き延びている言語になったのだと私は思います。
Cは高級言語にしては珍しく、ハードウェアを直接触れるからです。例えばソフトウェアでハードウェアを操作するときには、特定のアドレスに命令を表す特定のデータを書き込むと言う操作をします。これがソフトとハードの境目なのですが、多くの高級言語では「開発者にハードウェアを意識させないのが良い」という思想なので、ハードウェアのアドレスを指定できないようになっています。
ところがCの場合はポインタ変数を使って任意のアドレスに直接書き込めるのです。この機能を使わなければハードウェアが操作できないので、他の言語では実装できないのです。
一応これはC++でもできます。しかしC++が使える組み込みエンジニアは多くありません。
それは組み込み系のエンジニアはもともとはハード屋さん出身の人も多く、ソフトウェアの知識は限定的なことが多いからです。
特にオブジェクト指向やらデザインパターンやらのソフトウェア工学の話は何回聞いてもさっぱりという組み込みエンジニアは多く、そのような技法を現場に持ち込むと黒船扱いされます。
私はそのようなハード屋に近いプログラマーと仕事をしたことがありますが、継承とかポリモーフィズムのような実際のハードウェアの動きをソフト的に抽象化されるとさっぱり理解できないようでした。
ベテランの方はCは簡単とおっしゃいますが、最近また難しくなってきたんですよ。オプティマイザが言語仕様ギリギリを攻めるようになってきたんで、「ポインタなんてメモリのアドレスでしょ」という認識だとハマります。
初心者が基礎的な範囲内で使ってる分には滅多に落とし穴は踏まないと思うんですが、中級者になってそこを踏んでしまうと、なぜ動かないかのを理解するのに分厚い言語仕様を調べるハメになるんで、スパルタンに鍛えられるという意味では難しいと言えるかと。
ごく最近もこういう事例に当たりました。20年以上広く使われてきたOSSのライブラリが、新しいコンパイラで以前と異なる結果を返すようになって調査したんです。原因はここでした。
- void *buf56 = &context->s256.buffer[56];
- *(sha_word64*)buf56 = context->s256.bitcount;
- /* Final transform: */
- SHA256_Internal_Transform(context, (sha_word32*)context->s256.buffer);
context->s256.bitcountは64ビットワードで、最初の2行はそれをバッファの56オクテット目からネイティブバイトオーダーで格納する、というのを意図して
ベテランの方はCは簡単とおっしゃいますが、最近また難しくなってきたんですよ。オプティマイザが言語仕様ギリギリを攻めるようになってきたんで、「ポインタなんてメモリのアドレスでしょ」という認識だとハマります。
初心者が基礎的な範囲内で使ってる分には滅多に落とし穴は踏まないと思うんですが、中級者になってそこを踏んでしまうと、なぜ動かないかのを理解するのに分厚い言語仕様を調べるハメになるんで、スパルタンに鍛えられるという意味では難しいと言えるかと。
ごく最近もこういう事例に当たりました。20年以上広く使われてきたOSSのライブラリが、新しいコンパイラで以前と異なる結果を返すようになって調査したんです。原因はここでした。
- void *buf56 = &context->s256.buffer[56];
- *(sha_word64*)buf56 = context->s256.bitcount;
- /* Final transform: */
- SHA256_Internal_Transform(context, (sha_word32*)context->s256.buffer);
context->s256.bitcountは64ビットワードで、最初の2行はそれをバッファの56オクテット目からネイティブバイトオーダーで格納する、というのを意図してます。その後バッファを別の関数に渡しています。
最近のコンパイラ使ってる方はもうピンと来たと思いますが。gcc9だと、「bitcountをバッファに格納する」というインストラクションが出力されません。無かったことになります。
なお、SHA256_Internal_Transformの呼び出しの直前に別の関数呼び出しを入れると、「bitcountをバッファに格納する」というインストラクションが出力されるようになります。
この挙動を説明するには、strict aliasing ruleとオプティマイザの気持ちを説明しなければならず、中級者になったばかりくらいの人にそれを説明するのはちと大変だなあと思うわけです。
私見です。
私がいつも(特に私が若い頃に当時高齢者層のおっさんに)説明してたのは、
C(C++も)はミッション車です。ATのようには運転できません、です。
もう少し説明する場合は、
最近の高級言語って、AT車みたくハンドル・アクセル・ブレーキの操作が分かれば動かせます。
なんなら衝突防止とかSBSとか、多少手荒な事をしても自動でやってくれる装置がいっぱい使える車もあります。
が、
Cは低級言語と云われてて、シフト・クラッチと同じく、車が進む仕掛けに近い部分を理解して、扱えないと(作れないし)動きません。なんならそっちの方が大事で、面倒で、解ってないと車壊すか事故起こすのも同じ。
その代わり、AT車では出来ないような事が出来ます。ダブルクラッチとかLOWに入れてエンブレ利かすとか。
でも、うまく加減してやれないと、かぶってエンストやら、使いすぎたクラッチが滑ってすっぽ抜けてとか、そういう事が起こります。そういう加減がシロウトには難しい言語です。
ただ、勝手にギア比が変わって、、みたいな想定外もない。(想定外が起こったけど解決出来ない、のはシステム的にはやっかいです)
と云ってました。
ただ、これってAT免許なんてなかった時代に免許取った(つまり今8t限定中型免許)の世代にはけっこう有効で、ついでにダブルクラッチやら昔ばなしに花が咲いておっさん客と仲良くなれるし、しかもそれを得意になって社内で話してく
私見です。
私がいつも(特に私が若い頃に当時高齢者層のおっさんに)説明してたのは、
C(C++も)はミッション車です。ATのようには運転できません、です。
もう少し説明する場合は、
最近の高級言語って、AT車みたくハンドル・アクセル・ブレーキの操作が分かれば動かせます。
なんなら衝突防止とかSBSとか、多少手荒な事をしても自動でやってくれる装置がいっぱい使える車もあります。
が、
Cは低級言語と云われてて、シフト・クラッチと同じく、車が進む仕掛けに近い部分を理解して、扱えないと(作れないし)動きません。なんならそっちの方が大事で、面倒で、解ってないと車壊すか事故起こすのも同じ。
その代わり、AT車では出来ないような事が出来ます。ダブルクラッチとかLOWに入れてエンブレ利かすとか。
でも、うまく加減してやれないと、かぶってエンストやら、使いすぎたクラッチが滑ってすっぽ抜けてとか、そういう事が起こります。そういう加減がシロウトには難しい言語です。
ただ、勝手にギア比が変わって、、みたいな想定外もない。(想定外が起こったけど解決出来ない、のはシステム的にはやっかいです)
と云ってました。
ただ、これってAT免許なんてなかった時代に免許取った(つまり今8t限定中型免許)の世代にはけっこう有効で、ついでにダブルクラッチやら昔ばなしに花が咲いておっさん客と仲良くなれるし、しかもそれを得意になって社内で話してくれたりして、こっちペースで廻せるきっかけになるいい手だったんですが、
若い子には解らないみたいです。
今なら、何に例えればいいんでしょうね、、
セミコロンは文と文を区切る区切子(デリミタ)ではなく、ましてや、単なる飾りでもありません。C言語において構文上の重要な役割を持っています。
セミコロンは文の一種である式文(expression statement)を構成する終端記号です。もしセミコロンがなければ文とはならず、式(expression)として扱われます。
C言語が登場した1970年代当時、他の言語と一線を画した機能の一つに代入式があります。代入式はALGOLが発祥ですが、商用で成功したプログラミング言語ではC言語をもって嚆矢とします。当時主流だった多くのプログラミング言語には代入文はあっても代入式はありませんでした。この二つは似て非なるもので、文法上の位置づけが異なります。
例えばC言語で y = x * 0.5 と書けば、これはセミコロンで終わっていないので代入文ではなく、代入式と呼ばれる式の一種でしかありません。代入式は「左辺式 = 右辺式」という形をしており、
- 左辺式が指し示すオブジェクトに、右辺式の値を代入するとともに
- そのオブジェクトの代入後の値を代入式自身の値とする
という働きをします。y = x * 0.5 の例では、仮に変数 x の値が 3.0 だとすれば、右辺の乗算式 x * 0.5 の計算結果すなわち 1.5 が変数 y に代入され、さらに、代入後の変数 y の値すなわち 1.5 が代入式 y = x * 0
セミコロンは文と文を区切る区切子(デリミタ)ではなく、ましてや、単なる飾りでもありません。C言語において構文上の重要な役割を持っています。
セミコロンは文の一種である式文(expression statement)を構成する終端記号です。もしセミコロンがなければ文とはならず、式(expression)として扱われます。
C言語が登場した1970年代当時、他の言語と一線を画した機能の一つに代入式があります。代入式はALGOLが発祥ですが、商用で成功したプログラミング言語ではC言語をもって嚆矢とします。当時主流だった多くのプログラミング言語には代入文はあっても代入式はありませんでした。この二つは似て非なるもので、文法上の位置づけが異なります。
例えばC言語で y = x * 0.5 と書けば、これはセミコロンで終わっていないので代入文ではなく、代入式と呼ばれる式の一種でしかありません。代入式は「左辺式 = 右辺式」という形をしており、
- 左辺式が指し示すオブジェクトに、右辺式の値を代入するとともに
- そのオブジェクトの代入後の値を代入式自身の値とする
という働きをします。y = x * 0.5 の例では、仮に変数 x の値が 3.0 だとすれば、右辺の乗算式 x * 0.5 の計算結果すなわち 1.5 が変数 y に代入され、さらに、代入後の変数 y の値すなわち 1.5 が代入式 y = x * 0.5 の値となります。
これの何がスゴイかというと、代入式は式の一種なので別の式の中に代入式を含ませることができるのです。例えば z = y = x = 1.0 という式が書けるのです。これは、括弧を使って演算子の優先順位を分かりやすく示せば、 z = (y = (x = 1.0)) と書いたのと同じです。x = ~、y = ~、z = ~ という3つの代入式が入れ子になっており、いずれの代入式の値も 1.0 で、結果として、x, y, z のそれぞれに 1.0 を代入したのと等価になります。
さらには、sqrt(a = (b = 6.0) + 10.0) なんて式も書けます。これは、変数 b に 6.0 を代入し、6.0 + 10.0 を計算して変数 a に 16.0 を代入し、さらに sqrt(16.0) を計算して 4.0 を返す式です。FORTRANやBASICではこんな芸当はとてもできません。最近流行りの Python ですら代入式が導入されたのは最近(バージョン3.8)からであり、しかも代入式が使える箇所は限定的です。
このように、C言語ではそれまでの式の概念を大幅に拡張しました。そして、任意の式の末尾にセミコロンを付けた構文「式 ; 」を式文と定義し、文の一種としました。プログラム上の意味があるかどうかは別として、以下の各行はいずれも式文です。
- a = 3.0;
- 1.0 + 4.0;
- b;
- b + c;
- sqrt(x);
30年以上前の自分を思い出しました.懐かしいなー.同じようなことを考えて,当時住んでいたアパートにほど近い,南阿佐ヶ谷駅の横にあった本屋に行き,Cのソースコードがいちばんたくさん掲載されている本を買うことにしました.そこで出会ったのがMINIXオペレーティング・システムです.
この本でUNIX文化の一端に触れたことは,その後の自分に大きな影響を与えました.UNIXやLinuxをある程度知っているふりができるのもこの本と,この本をきっかけとしてその後手にすることになる関連本のおかげです.あの本屋はもうなくなってしまったようですが,当時あの狭くてごちゃごちゃした本屋でこの本にばったり出会ったことは,思い返してみると幸運なことでした.
というわけで,「このあとどうすればいいですか?」に対する私なりの答えは「本屋さんに行ってみよう」です.
ただ残念なことに,いま本屋に行ってC言語やそれに関連した本を探しても,ソースコードが何10ページにも渡って掲載されているような本は見つからないでしょう.…えっ,ああ,そうです.上のMINIX本にはMINIXのソースコードがすべて掲載されていたので,ほんとうに何10ページもCのプログラムが載っていたわけです.私がこの本を買ったのは1989年のことですが,当時のパソコン関連本はいまほど人口に膾炙
30年以上前の自分を思い出しました.懐かしいなー.同じようなことを考えて,当時住んでいたアパートにほど近い,南阿佐ヶ谷駅の横にあった本屋に行き,Cのソースコードがいちばんたくさん掲載されている本を買うことにしました.そこで出会ったのがMINIXオペレーティング・システムです.
この本でUNIX文化の一端に触れたことは,その後の自分に大きな影響を与えました.UNIXやLinuxをある程度知っているふりができるのもこの本と,この本をきっかけとしてその後手にすることになる関連本のおかげです.あの本屋はもうなくなってしまったようですが,当時あの狭くてごちゃごちゃした本屋でこの本にばったり出会ったことは,思い返してみると幸運なことでした.
というわけで,「このあとどうすればいいですか?」に対する私なりの答えは「本屋さんに行ってみよう」です.
ただ残念なことに,いま本屋に行ってC言語やそれに関連した本を探しても,ソースコードが何10ページにも渡って掲載されているような本は見つからないでしょう.…えっ,ああ,そうです.上のMINIX本にはMINIXのソースコードがすべて掲載されていたので,ほんとうに何10ページもCのプログラムが載っていたわけです.私がこの本を買ったのは1989年のことですが,当時のパソコン関連本はいまほど人口に膾炙していませんでしたから,尖った本が多かったのです.いまはそういう本が少なくて物足りない気がします.
ですので,専門書をたくさん置いてある大型書店に行くのがよいかと思います.あるいはちょっと古めの洋書を探すとか.尖っている本に出会ってほしいと思います.
参考になれば幸いです.
偉くないです。今なら Kotlin とか、Python とかが偉いかも知れません。あと、Javascript/Typescript がしぶとく粘って、その隙を Go と Rust が窺ってるって感じ。あと、何気に C# が存在感を放ってますね。C? ああ、あのロートル?
ただし、組み込みとか OS 開発とかの現場ではヒーローです。他の言語は相手になりません。C++ が必死になって対抗していますが、すぐバイナリを肥大化させるので、C に太刀打ちできてません。Objective-C? 残念ながら macOS のカーネルって、C なんですよ。I/O Kit だけは C++ です。
そういえば、Windows が C++ を使って開発されていますが、カーネルが巨大化してる上にサブシステムも巨大です。組み込み機器にはそんな巨大な空間はないのです。一時期 Microsoft はやっきになって組み込み向け Windows を宣伝してましたが、撤退しました。
Unix/Linux カーネルは C ですが、それでも結構なサイズなので厳しい世界です。往事の XFree86 とか、X.Org も C でしたね。それで無理くりオブジェクト指向もどきを実装したのでコードが大変複雑でした。Linus が F○ck! とか言いかねないレベルです。
まあ向き不向きがあるってことで。
システム系も Web 系もごったまぜで比
偉くないです。今なら Kotlin とか、Python とかが偉いかも知れません。あと、Javascript/Typescript がしぶとく粘って、その隙を Go と Rust が窺ってるって感じ。あと、何気に C# が存在感を放ってますね。C? ああ、あのロートル?
ただし、組み込みとか OS 開発とかの現場ではヒーローです。他の言語は相手になりません。C++ が必死になって対抗していますが、すぐバイナリを肥大化させるので、C に太刀打ちできてません。Objective-C? 残念ながら macOS のカーネルって、C なんですよ。I/O Kit だけは C++ です。
そういえば、Windows が C++ を使って開発されていますが、カーネルが巨大化してる上にサブシステムも巨大です。組み込み機器にはそんな巨大な空間はないのです。一時期 Microsoft はやっきになって組み込み向け Windows を宣伝してましたが、撤退しました。
Unix/Linux カーネルは C ですが、それでも結構なサイズなので厳しい世界です。往事の XFree86 とか、X.Org も C でしたね。それで無理くりオブジェクト指向もどきを実装したのでコードが大変複雑でした。Linus が F○ck! とか言いかねないレベルです。
まあ向き不向きがあるってことで。
システム系も Web 系もごったまぜで比較したら以下のような結果になるようです。
他の言語にはあまりない概念だからです。
- int *x;
と書いても実際にint型の値をもった変数が作られるわけではないというのは他言語を先に勉強した人からすると混乱すると思います。また、
- int x;
- int *y = (int *)malloc(sizeof(int));
の違いも混乱するでしょう。大抵の言語では基本的にプリミティブ型以外の構造体などので変数は関数に参照渡しで渡されますが、Cではポインタを使わない限りは値渡しで関数内ではコピーになるため、関数内でメンバ変数の値を変えてもそれが元の構造体変数には反映されないという点も始めは難しいのではないでしょうか。
次に配列とポインタの関係が挙げられます。
- int x[10];
とした時、xというシンボルは配列の先頭のアドレスを指すポインタとか言われても、データがメモリ上に連続で配置されているというようなハードウェアに近い知識がないと最初は意味がわかりません。
- int x[10];
- int *y = (int *)malloc(sizeof(int)*10);
のように同じようで微妙に違うのも困ったもんです。(前者はstack,後者はheap)
ポインタのポインタで2次元配列を扱えるけどイコールではないのも数値計算をやっていた自分は始め困惑していました。
あとはポインタのキャストもなんで必要なのか初学者が理解するのは難しいように思えます。
要はポインタは自由
他の言語にはあまりない概念だからです。
- int *x;
と書いても実際にint型の値をもった変数が作られるわけではないというのは他言語を先に勉強した人からすると混乱すると思います。また、
- int x;
- int *y = (int *)malloc(sizeof(int));
の違いも混乱するでしょう。大抵の言語では基本的にプリミティブ型以外の構造体などので変数は関数に参照渡しで渡されますが、Cではポインタを使わない限りは値渡しで関数内ではコピーになるため、関数内でメンバ変数の値を変えてもそれが元の構造体変数には反映されないという点も始めは難しいのではないでしょうか。
次に配列とポインタの関係が挙げられます。
- int x[10];
とした時、xというシンボルは配列の先頭のアドレスを指すポインタとか言われても、データがメモリ上に連続で配置されているというようなハードウェアに近い知識がないと最初は意味がわかりません。
- int x[10];
- int *y = (int *)malloc(sizeof(int)*10);
のように同じようで微妙に違うのも困ったもんです。(前者はstack,後者はheap)
ポインタのポインタで2次元配列を扱えるけどイコールではないのも数値計算をやっていた自分は始め困惑していました。
あとはポインタのキャストもなんで必要なのか初学者が理解するのは難しいように思えます。
要はポインタは自由度が高すぎるんですよ。C++の参照くらいでちょうどいいし、実際殆どのポインタの用途はそれのはず。
あ、もう一つありますね。NULLのポインタ指している値を参照しようとするとJavaのnull pointer exceptionと同じように落ちます。ここまではいいのですが、でたらめなアドレスを指しているポインタの値を参照しても落ちないし、そのメモリの値を変えることすらできます。なのでポインタを使う場合はしつこく初期化やnullチェックに気をつけないといけません。
Arduino(アルディーノ)をお勧めします。
小型マイコンボードです。USBでパソコンと繋ぐだけで使えます。開発環境も無料です。色々なセンサーや無線モジュール乗っけると今流行りのIoT、組込インターネットデバイスが作れます。
開発言語は「C言語」です。以下のリファレンスが読めればOKです。
Arduinoは色々種類がありますがお勧めは「MakerUNO」ですね。
秋月電子の通信販売でクレジットカード、銀行振込、代引き(商品受け取るときにお金を払う)で買えます。
https://akizukidenshi.com/catalog/g/gM-16285/利点は以下3点です。
①安い 通常のArduinoUNOが3000円くらいなのにMakerUNOは「720円」
②最初からLEDが13個、圧電スピーカー、メカスイッチがついている。
③ArduinoUNO互換機。だからArduinoUNOのプログラムがそのまま動く。Arduioはオープンハードウェアだから色んな所が作ってます。
元々このMakerUNOはマレーシアで子供の教育用に考えて作られているので色々な機能が最初から入っています。
電源入れるとお馴染みのテーマが流れるでしょう。
Micro USBケーブルは付いてないので一緒に買って下
Arduino(アルディーノ)をお勧めします。
小型マイコンボードです。USBでパソコンと繋ぐだけで使えます。開発環境も無料です。色々なセンサーや無線モジュール乗っけると今流行りのIoT、組込インターネットデバイスが作れます。
開発言語は「C言語」です。以下のリファレンスが読めればOKです。
Arduinoは色々種類がありますがお勧めは「MakerUNO」ですね。
秋月電子の通信販売でクレジットカード、銀行振込、代引き(商品受け取るときにお金を払う)で買えます。
https://akizukidenshi.com/catalog/g/gM-16285/利点は以下3点です。
①安い 通常のArduinoUNOが3000円くらいなのにMakerUNOは「720円」
②最初からLEDが13個、圧電スピーカー、メカスイッチがついている。
③ArduinoUNO互換機。だからArduinoUNOのプログラムがそのまま動く。Arduioはオープンハードウェアだから色んな所が作ってます。
元々このMakerUNOはマレーシアで子供の教育用に考えて作られているので色々な機能が最初から入っています。
電源入れるとお馴染みのテーマが流れるでしょう。
Micro USBケーブルは付いてないので一緒に買って下さい。
NULLをマクロでなくキーワードとする(キーワードなら小文字が適切ですが)
整定数をポインタにできる、ということと、整定数0をヌルポと解釈されるの両立は無理すぎます。c++ができたとき、型チェックを厳しくしようとしたのにc言語のヌルポの仕様のお陰でヌルポだけはかえってややこしくなった。(C言語なら、まだNULLを ((void *)0)と展開すればヌルポであることがコンパイラに伝わったが、C++ではこれができなくなった)。
質問の意図は「何を理解すればC言語が簡単に思えるのか?」だと思いました。
C言語は難しい、はあまり正確な表現ではないでしょう。より正確には
- C言語は簡単だけど、使い熟すのが難しい
と理解すれば、何を理解すれば簡単になるのか?も分かります。
現在ある数えきれないプログラミング言語に比べ、C言語の仕様は「簡単かつ単純」です。C言語の仕様は難しくないです。暗記が必要な言語仕様の数は高級言語の中では最小の部類だと思います。(今時Cは高級言語ではないとか、純粋関数型言語の方が単純だとか、は置いておきます)
C言語が難しいとされる最大の原因は以下の4つではないでしょうか?
- プログラマがやりたい事を「実装する為の注意点が多い」こと(メモリ管理とポインタ管理)
- その注意点は「他の言語では理解していなくても実装できてしまう」こと
- 注意不足による問題の多くが「どこが問題の発生箇所か簡単には判らない」こと
- 更に仕様が単純であるため、他の言語では「単なる機能である物もコーディングしなければならない」こと
まとめると
- C言語は単純で覚えるのは容易な言語
- ただし、使い熟すには技術が必要
であるため「C言語は難しい」とよく言われるのだと思います。
- メモリ管理
- ポインタ管理
- デバッグ
この三つを使いこなす技術があれば、「いちいちコードを書くのが面倒臭い」とは思っても、他の言語に比べて難しいとは思わなくなると思います。
確実なメモリ管理/ポインタ管理
質問の意図は「何を理解すればC言語が簡単に思えるのか?」だと思いました。
C言語は難しい、はあまり正確な表現ではないでしょう。より正確には
- C言語は簡単だけど、使い熟すのが難しい
と理解すれば、何を理解すれば簡単になるのか?も分かります。
現在ある数えきれないプログラミング言語に比べ、C言語の仕様は「簡単かつ単純」です。C言語の仕様は難しくないです。暗記が必要な言語仕様の数は高級言語の中では最小の部類だと思います。(今時Cは高級言語ではないとか、純粋関数型言語の方が単純だとか、は置いておきます)
C言語が難しいとされる最大の原因は以下の4つではないでしょうか?
- プログラマがやりたい事を「実装する為の注意点が多い」こと(メモリ管理とポインタ管理)
- その注意点は「他の言語では理解していなくても実装できてしまう」こと
- 注意不足による問題の多くが「どこが問題の発生箇所か簡単には判らない」こと
- 更に仕様が単純であるため、他の言語では「単なる機能である物もコーディングしなければならない」こと
まとめると
- C言語は単純で覚えるのは容易な言語
- ただし、使い熟すには技術が必要
であるため「C言語は難しい」とよく言われるのだと思います。
- メモリ管理
- ポインタ管理
- デバッグ
この三つを使いこなす技術があれば、「いちいちコードを書くのが面倒臭い」とは思っても、他の言語に比べて難しいとは思わなくなると思います。
確実なメモリ管理/ポインタ管理を行うのが難しいので、何時まで経っても簡単だ、とは思えない可能性が高いですが。
今時の言語は「Write once, Run everywhere」という感じですが、C言語でクロスプラットフォームな実用プログラムを書こうとすると大変です。この観点からC言語の難しさを考えると
- 仕様が定義されていなくて、処理系依存の物がある
があります。
「このコンパイラでは思った様に動作するにのに、こっちのコンパイラでは思った様に動作しない」「このCPUでは思った様に動作するのに、こっちのCPUでは思った様に動作しない」といったことが起こります。
- ライブラリの差異
も難しく感じる原因になっていると思います。大量の#ifdefが必要になったります。難しいというより「面倒臭い」「手間がかかりすぎ」という問題ですが、実際にクロスプラットフォームなプログラムを書く場合の困難さはかなり大きいです。
これらのクロスプラットフォームで動作するプログラムの記述がかなり面倒だという現実も「C言語が難しい」と感じる原因の一つだと思います。
しかし、これらは「C言語が難しい」のではなく「クロスプラットフォームなプログラムを書くには手間がかかりすぎる」が問題で、C言語習得の難易度とは異なる問題だと思います。
C言語より前にも例えばPascalやLispはスタック領域・ヒープ領域を前提とした言語仕様ですので、これらは存在したことは間違いないです。
しかし、IBM360にはCPUの概念としてスタック領域・ヒープ領域という概念は有りません。もちろん、メモリは有るのでこれらをシミュレートすることは出来ますが。
スタックという概念が生まれたのが1955年とのことです。
ヒープ領域というのがメモリのある領域を割り当てたり、解放することのできるメモリ管理(Region-based memory management)が出来る領域だとすると、発明されたのは1967年とのことです。
Cは今でも非常に明確で安定した役割を持つ、現代でもよく使われている言語の一つです。45年という人気のコンピュータ言語の中でも最長老級の言語でありながら、Cは今でも第9位と意外に高いシェアを保っています。(GitHubより)
面白いことに、Cの最大の武器は「低機能であること」です。どんどん機能を追加して便利になる数多くのコンピュータ言語に囲まれながら、唯一独自の路線をいく言語と言えます。
そんなCの現代の用途として真っ先に挙げるべきなのは、「コンピュータ言語の開発」です。
実は、トップクラスの知名度を誇るPython、Ruby、PHPの実行環境は全てCで開発されています。つまり様々なスクリプト言語の活躍は、この分野で圧倒的なシェアを持つCが今も背後で支えて成り立っているのです。(JavaScriptのV8はC++)
C「大地と共に生き、自らの足で大地を駆ける、それが私達だ」(画像はBing Image Creatorで生成)
自動化機能をほとんど持たないCは、メモリやハードウェアの管理をほぼ全て手作業で行う必要があります。その代わり、プログラマーの頭の中に思い浮かんだどんなに複雑で難解なデータ構造やアルゴリズムも、記述通りメモリ上に理想的な配置で表現し、挙動を完全に制御できます。さらに速度が必要な場合はコード内にアセンブリ言語を直接記述できます。何もかも手作業な上にミスが許されないため、Cは利
Cは今でも非常に明確で安定した役割を持つ、現代でもよく使われている言語の一つです。45年という人気のコンピュータ言語の中でも最長老級の言語でありながら、Cは今でも第9位と意外に高いシェアを保っています。(GitHubより)
面白いことに、Cの最大の武器は「低機能であること」です。どんどん機能を追加して便利になる数多くのコンピュータ言語に囲まれながら、唯一独自の路線をいく言語と言えます。
そんなCの現代の用途として真っ先に挙げるべきなのは、「コンピュータ言語の開発」です。
実は、トップクラスの知名度を誇るPython、Ruby、PHPの実行環境は全てCで開発されています。つまり様々なスクリプト言語の活躍は、この分野で圧倒的なシェアを持つCが今も背後で支えて成り立っているのです。(JavaScriptのV8はC++)
C「大地と共に生き、自らの足で大地を駆ける、それが私達だ」(画像はBing Image Creatorで生成)
自動化機能をほとんど持たないCは、メモリやハードウェアの管理をほぼ全て手作業で行う必要があります。その代わり、プログラマーの頭の中に思い浮かんだどんなに複雑で難解なデータ構造やアルゴリズムも、記述通りメモリ上に理想的な配置で表現し、挙動を完全に制御できます。さらに速度が必要な場合はコード内にアセンブリ言語を直接記述できます。何もかも手作業な上にミスが許されないため、Cは利用者の高い技量が求められる極めて難易度の高い言語ですが、ハードウェアを完璧に乗りこなし、究極の演算効率を目指す熟練プログラマーにとっては唯一無二の言語なのです。
Cのもう一つの大切な用途は、「小規模組み込み系開発」です。
Cは長年、リモコンや炊飯器などOSを介さずに動作する小規模なハードウェア制御プログラムの開発でよく使用されています。これら電化製品に組み込まれた低性能なチップ上で動作できるのはアセンブリかCだけです。リモコンや炊飯器など、家にある小型の電化製品のほとんどはCのプログラムで制御されていると思って良いでしょう。
特に、乾電池1、2本で数年間動作する超省電力なリモコンのように、身の回りに当たり前にあるにも関わらず、今の所Cかアセンブリ以外では書きたくても書けないプログラムも実はたくさんあります。逆に組み込み系でもそれなりな性能のチップで省電力にあまり気を使わなくて良いものや、ある程度以上大規模なものはC++で開発されていることが多いです。
ちなみに、よく「C/C++」と書かれて混同されがちなCとC++ですが、これら2つの言語は全くの別物です。
C++は次々と先進的な機能を取り入れて成長する「最新の言語」の1つですが、Cは「機能的に進化しない言語」です。Cはめったに(10年以上)バージョンアップが起きない上に、マイナーレベルのアップデートしかしません。この2つの言語はそれぞれ全く異なった目的と意義を持った言語ですので、ちゃんと別々に扱ってあげて下さい。
Cは時代に流されずに独自の文化を守る、誇り高い孤高の言語です。また私たちのすぐそばで働く身近な言語でもあります。忘れ去られた言語などでは全くありませんので悪しからず。
プログラミング言語は、大まかにインタプリタ言語とコンパイル言語に分けることができます。
インタプリタ言語はRuby,Python,PHP,JavaScriptのような言語で、コンパイル言語はJava,C,C++,C#,Go,Rustのような言語です。
どのプログラミング言語も、最終的には0と1のビットの羅列しか処理できないCPUを駆動しているのですから、人間が入力した文字を何らかの方法でビット列に変換しています。このビット列をマシン語といいます。マシン語はCPUのメーカーごとに規格が違うので、IntelのCPU用のマシン語ファイルをARMのCPUで動かすことはできません。
プログラムを動作させているときにマシン語に変換するのがインタプリタ言語で、プログラムを動作する前にマシン語に変換して、ファイルに保存しておき、実行時にマシン語への変換をしないのがコンパイル言語といいます。
一般にコンパイル言語のほうがだいぶ高速ですが、インタプリタ言語ならばCPUの種類によらず動きます。
コンパイル言語は、CPUの種類に依存しないバイトコードにいったん変換しておく言語とそうでない言語にわかれます。JavaやC#などはバイトコードを使う言語で、C/C++/RustやGolangのように直接マシン語に変換します。バイトコードを使う言語は、実行時にマシン語に変換します。バイトコードを使う言語は、基本的には、インタ
プログラミング言語は、大まかにインタプリタ言語とコンパイル言語に分けることができます。
インタプリタ言語はRuby,Python,PHP,JavaScriptのような言語で、コンパイル言語はJava,C,C++,C#,Go,Rustのような言語です。
どのプログラミング言語も、最終的には0と1のビットの羅列しか処理できないCPUを駆動しているのですから、人間が入力した文字を何らかの方法でビット列に変換しています。このビット列をマシン語といいます。マシン語はCPUのメーカーごとに規格が違うので、IntelのCPU用のマシン語ファイルをARMのCPUで動かすことはできません。
プログラムを動作させているときにマシン語に変換するのがインタプリタ言語で、プログラムを動作する前にマシン語に変換して、ファイルに保存しておき、実行時にマシン語への変換をしないのがコンパイル言語といいます。
一般にコンパイル言語のほうがだいぶ高速ですが、インタプリタ言語ならばCPUの種類によらず動きます。
コンパイル言語は、CPUの種類に依存しないバイトコードにいったん変換しておく言語とそうでない言語にわかれます。JavaやC#などはバイトコードを使う言語で、C/C++/RustやGolangのように直接マシン語に変換します。バイトコードを使う言語は、実行時にマシン語に変換します。バイトコードを使う言語は、基本的には、インタプリタ言語より速いけど、マシン語に直接変換する言語より速いです。バイトコードを使えば、インタプリタ言語とコンパイル言語の間のようなものを作れるということです。
実はここまで、大事なことを書かずにいました。上記に挙げたような著名な言語については、ひとつの言語について、処理系がたくさんあります。たとえばRubyであれば、MRI, JRuby, IronRuby, mrubyなど10以上、小さいのもふくめたらもっとあります。それらには、インタプリタ処理系もバイトコード処理系もコンパイルをする処理系もあります。 ( Rubyアソシエーション: Ruby処理系の概要 ) MRIは以前はインタプリタ言語でしたが、いまはバイトコード言語になっています。重要なのは、言語仕様と処理系の仕様は独立しているということです。
Rubyの処理系で私が期待しているのは、Rubyの言語仕様を一部削減したサブセット言語を定義し、それを直接マシン語に変換するmmc という処理系です。 ( Ruby 3 の型解析に向けた計画 ) このようにインタプリタ言語やバイトコード言語の言語仕様のサブセットを定義してマシン語へのコンパイルをする処理系のひとつには、UnityのC# Burstコンパイラがあります。 WebAssembly規格もそうしたもののひとつかもしれません。
上記をまとめると、現在は、言語仕様と処理系の仕様は独立しているので、ある言語を何のために使いたいかによって、必要な機能をもつ処理系を使い分けると良い、ということです。
概念的なものでしたら、仮想メモリーや、動的メモリー割合てに関連する技術とその仕組みのことですので、20世紀後半ぐらいに、ちょうどコンピューター黎明期にあたる時期に様々なアイデアが生まれ、実装されています。
その後、「ヒープ」と「スタック」のような固有名称として認識が広まりはじめるのは、C言語やJavaが増えつつ時代ですので、だいたい20世紀末期ぐらいだろうと思いますで。
もともと、メモリーへのスタックの考え方などは、C言語が無かった時代でも、そのままアセンブリ言語では、レジスタの値を退避するため、PUSH POP命令を使ってスタック動作を実現しています。
たたし、これだと直前に対比したレジスタの値を戻のには便利ですが、複数のレジスタが必要なサブルーチンでは何かと都合が悪いのです。
そこで、スタックの開始位置をCPU任せにしない方法として、色々な実装方法が利用されていました。その色々な方法を「スタック」ではない別の方法のひとつに、ヒープと同様の仕組みもありました。
あの時代が必要とした要件(高速性、高効率な実行を実現するために最適化コンパイラをさほど必要としない低抽象度の実行モデル)を、適切なタイミングで提供したから、でしょうか。
高級過ぎて低レベル操作がしにくかったり、最適化が必要でコンパイルに時間がかかる言語などは、時代の要請にはマッチしませんでした。
さらに、「その時代の優秀な人材を引き付ける」というのは、成功する言語には必須の条件に思います。当時、UNIXが隆盛をむかえようとしていて、OSやネットワーキングの技術で世界をかえていこうという優秀な人材が、UNIXに近づき、その実装言語であったC言語のアクティブユーザーになっていった、みたいな動向もCの発展・利用者拡大に寄与した要素であろうと思います。言語が優秀なだけではなく、利用者が優秀であることが言語の成功にとって重要です。
プログラミング言語界ということで今でもC言語が得意な低レイヤは除いてプログラミング言語だけで考えても、まだまだ影響力があるという意味では偉いです。
Rubyの中を覗くとC言語で書いてる(例:object.c)
PHPの中を覗くとC言語で書いてる(例:zend_objects.c)
Pythonの中を覗くとC言語で書いてる(例:object.c)
C言語でコード書く機会はまったくないとしても、C言語で書かれたコードの恩恵をまったく受けていないと言い切れるプログラマは少ないはず。
コンパイラ基盤であるLLVMやJavaScriptのエンジンV8なんかはC++が多めです。CだけじゃなくC++も含めて考えたらさらに偉すぎです。
JavaとかC#とかは自身の言語で書かれている割合が高め。そしてRustは(LLVM除けば)Rust自身、つまりセルフホスティング言語。Rustしか書いてないなら「C言語なにそれ」と言い張っても誰も突っ込めないでしょう。
日本の場合、一時期のCマガジンの存在が大きいかなって思ったりします。あれで育った現役プログラマーって結構多いと思いますよ。
フロッピーの時代から毎号付いてくるフリー版LSI C86に、年度が変わると第1回から始まるC言語入門、ちょっと背伸びすれば入門者でも手に届く中級者向けチョイスの特集に、上級者向けのマニアックな連載と、最高のバランスで構成された良い雑誌だったと思います。
プロ向けCが滅茶苦茶高かった時代に、MS-DOSが走るパソコンさえ持っていれば、誰でもすぐに勉強を始められた手軽さがよかったです。故に、最初の言語がCでも問題無かったのです。
int* a;とint *a;がどちらも正しいこと。意味論的にはint* aと解釈したいが、int* a,b;はバグを呼ぶコードで、int *a,*b;としないと構文上正しくない。
さらに*が参照解決にも使われるシンボルなので、ややこしい。同じシンボルに2つの意味(ポインタの宣言と参照解決)をつけたことで学習者の不安はマッハに。
素直にポインタや参照っていうのはタダのメモリアドレス(番地)のことだと言えばいいのに、参照やポインタなる深遠なる響きが実態と乖離した哲学へといちいち学習者を誘おうとする。(規格上はそう表現されているしC++を含めると参照とポインタは違うのでさらにややこしい。)
正直なところシンタックスが微妙なことが学習者の理解を大きく損ね続けている原因の一つだと考えられる。
1.ポインタと参照はメモリアドレスと読み替えてよい。
2.ポインタ変数を宣言する*と参照を解決する*は同じシンボルを使っているが違う意味だ。私は頭の中で後者をderef(参照剥がし)と呼んでいる。
3.意味的にはint*(int型への8byteまたは4byteのアドレス値)とみてよい、しかしint* a,b; int* aとint bの宣言になる。これはCの構文定義がおかしいと思う。(が規格化されているので抵抗する意味はない。) 自分で書くときはint* a; int* b;と分けて宣言することで正気を保つ。
4.
int* a;とint *a;がどちらも正しいこと。意味論的にはint* aと解釈したいが、int* a,b;はバグを呼ぶコードで、int *a,*b;としないと構文上正しくない。
さらに*が参照解決にも使われるシンボルなので、ややこしい。同じシンボルに2つの意味(ポインタの宣言と参照解決)をつけたことで学習者の不安はマッハに。
素直にポインタや参照っていうのはタダのメモリアドレス(番地)のことだと言えばいいのに、参照やポインタなる深遠なる響きが実態と乖離した哲学へといちいち学習者を誘おうとする。(規格上はそう表現されているしC++を含めると参照とポインタは違うのでさらにややこしい。)
正直なところシンタックスが微妙なことが学習者の理解を大きく損ね続けている原因の一つだと考えられる。
1.ポインタと参照はメモリアドレスと読み替えてよい。
2.ポインタ変数を宣言する*と参照を解決する*は同じシンボルを使っているが違う意味だ。私は頭の中で後者をderef(参照剥がし)と呼んでいる。
3.意味的にはint*(int型への8byteまたは4byteのアドレス値)とみてよい、しかしint* a,b; int* aとint bの宣言になる。これはCの構文定義がおかしいと思う。(が規格化されているので抵抗する意味はない。) 自分で書くときはint* a; int* b;と分けて宣言することで正気を保つ。
4.関数ポインタやポインタのポインタが出てきたら諦める。
『今でもC言語が使われている』???
『そんなわけがない!』
そんなものは、採用するICの大きさと制御の細かさで決めるのです。
デカイICでLinuxが載っててユーザーインターフェースなんていうトロトロタイミングなのにCを使う、なんて案件は単にロートルが牛耳っているだけです。
Linuxが乗るんならシェルスクリプトでもパイソンでもできるんじゃないか?(パイソン私使えないけどシェルスクリプト+awkっていう試作ならやった)
今の時代になっても、赤外線リモコンにLinuxは流石に載せませんよね、いやµITRONも載せませんよね、RAM数kB(数百Bかも)、フラッシュ数kB、クロック数百kHz、I/OはGPIOだけ(調歩同期シリアルも無いかも) 位の。あのPC-8001より小さな構成のマイコンに書くのなら、
Cでもきついかもしれない、スタック領域の確保で苦労するのでメモリマップと変数マップを丁寧に紙か表計算で書いて、アセンブラ直書きしないとできないかもしれません。
これがH8/300あたり、RAM16k~32kとかもっとになるとCがだいたい楽勝になって、
H8/500とかになるとOS載せたいね、ってITRON仕様のOSを買うか書くか、なんて昔はなっていて。
SHや68020とかになるとLinux載せられん?って言い出すわけですよ。
C#は知りません。c++について言えば、言語が巨大なので生きているうちに言語仕様の大まかな部分でさえ学べないだろうというところとか、オブジェクト指向などマルチパラダイム言語であることです。デストラクタをvirtualにすべきか否か?で迷いますし。
Effective c++ ぐらいをある程度覚えてプログラムを作れるようになれば私としては上出来ですが、テンプレートプログラミングなどをやろうとしたら、せっかく覚えたところを一旦忘れなければいけないらしいです。boostのような暗黒大陸に乗り込むのは怖すぎます。
c++ではc程マクロを使わずにすむのも大きいですね。cのマクロは柔軟ですが柔軟すぎていくらでも危険なプログラムをかけますし、デバッグも難しい。例外処理ができることも。
細かい違いとしては、'a' はCではint型ですがC++ではchar型です。またconstの扱いも違います。ソースファイルのトップレベルで const int nnn = 〜; とやった場合、cでは外部リンケージですがC++では内部リンケージです。その他細かな違いがあるので、C++はCの上位言語だと思いこんでいると落とし穴に引っかかると思います。
実は私の知ってるCは大昔のものなので間違いがあるかもしれませんが、c99とて互換性を保つため大昔のcの仕様を引き継いでいる筈だ、と考えて回答いたしました。
アラインされていないデータはCPUが自然な形でアクセスすることができなくなり、十分な速度が出せなくなるからです。
たとえば32bit CPUは32bit単位でRAMやキャッシュとデータのやりとりをしています。32bitにアラインされたデータであれば(アドレスの下位2ビットが0であれば)、1度の読み出しで読み出すことができるのですが、中途半端な位置に置かれていると、2回読み出したものを合体させて32bitのデータにしなければいけないので遅くなってしまいます。書き込みも同様です。
そもそも32ビットにアラインされていない32ビットのデータを読み出すことができないようなCPUも存在します。そのようなCPUでは、ユーザのプログラムでアライメントが理由でデータが読み出せなかったときにアライメント例外が発生します。その例外をOSがハンドルして、ユーザのプログラムがあたかもデータを読み出したかのように処理を行なうことも可能ですし、そうじゃなくプログラムを異常終了させてしまうことも可能です。前者の場合プログラムは動きますが動作が極めて遅くなります。
初心者ですが(何年初心者やってるのだろう?)
クラスの宣言ではpublicのものだけ宣言したくともprivateな部分の宣言も必要だったり。
shared_ptr を引数にするときは、const参照渡しがよいと聞いたが、それだと、結局shared_ptrの本体はどこにあるのか(ダングリングしてはまずい)に頭使ってしまったり。
デストラクタをvirtualにするべきかしないべきか判断が難しかったり。std :: input_iterator_tag は継承するクラスだけどvirtualなデストラクタがあったらまずいよね。STLコンテナをpublic継承したら危険だったり。たとえば、std::vector のイテレータはコピーコンストラクタが公開されているので、newで作ることができてしまったり。
最近は流行らないようですが、ポリモルフィックな使い方を予定しているクラスFooで必ずshares_ptr<Foo> かshared_ptr<Fooの子クラス>の形で使うことにするために、Fooのコンストラクタをprotectedにしたら、make_shared が使えなくなったり。
int main(int argc, char const * const * const argv)… というシグニチャを許容する処理系は沢山あるが、argvが書き換えられない保障はなかったり。
- class Foo {
- pr
初心者ですが(何年初心者やってるのだろう?)
クラスの宣言ではpublicのものだけ宣言したくともprivateな部分の宣言も必要だったり。
shared_ptr を引数にするときは、const参照渡しがよいと聞いたが、それだと、結局shared_ptrの本体はどこにあるのか(ダングリングしてはまずい)に頭使ってしまったり。
デストラクタをvirtualにするべきかしないべきか判断が難しかったり。std :: input_iterator_tag は継承するクラスだけどvirtualなデストラクタがあったらまずいよね。STLコンテナをpublic継承したら危険だったり。たとえば、std::vector のイテレータはコピーコンストラクタが公開されているので、newで作ることができてしまったり。
最近は流行らないようですが、ポリモルフィックな使い方を予定しているクラスFooで必ずshares_ptr<Foo> かshared_ptr<Fooの子クラス>の形で使うことにするために、Fooのコンストラクタをprotectedにしたら、make_shared が使えなくなったり。
int main(int argc, char const * const * const argv)… というシグニチャを許容する処理系は沢山あるが、argvが書き換えられない保障はなかったり。
- class Foo {
- private:
- Foo* const mythis = this;
- ....
- };
とかやると、mutable がなくとも、constメンバ関数で状態を書き換えられたり。
- #define private public
とかやると大変なことになったり。
その他そのた、うっかりミスをしやすいところ。
異常値を伝えるためにNULLを使うのはやめる。
基本データ型(char, int, short int, long int, long long int)のサイズを処理系依存にしない。
charは8bit固定
intにshortやlongの修飾語を付けるのも煩わしいので、int16やint32などビット数を明示する形が良いですね。
ベル研に火をつける。
intのバイトサイズだけは固定で。
スコープを抜ける際に、戻り値を除き、スコープ内で確保されたリソースを全て解放するようにする。