かなり初心者 Advent Calendar 1日目として
開いていたから入れました。初心者向け1日目としては重くもしかしたら不適当な内容かもしれませんが是非知っておいてほしいことなので遅刻ながら初日を取らせてもらいました。
この記事は自分の失敗談を元にした元記事があります、本当は元記事に追記してこの記事にしようと思いましたがあまりにC++に寄りすぎていてかなり初心者にそぐわないものになると感じたので別記事にしました。
しかし内容はプログラマーなら誰しもが知っておいて欲しい内容だと思うのであえて投稿します。
C言語は誰しもが知っておくべき、C++はプロ用だは間違い
しばしそのようなことが言われている気がしますが実は逆だと思います。C++はC言語の拡張で高機能と言われますがC++出できることは頑張ればC言語でできます。しかしそれにはかなりの危険が伴います、その一例がポインタでC++は参照という高機能なものがあるのでなるだけポインタというものは使わないようにと教えられます。
そしてしばしばC言語の入門サイトや入門書でポインタがあたかも参照であるかのような記述が見られます。しかし、ポインタと参照は全くの別物です、あえて誤解を恐れず言うならポインタはポイントしている(指している)値を参照できるものです。
では何故これが危険なのか見ていきましょう。
値渡しと参照渡し
参照渡しはC言語が生まれる以前からあります。COBOLやFORTRANというものすごく古い言語にもあります。これらが生まれたのが1960年前後、C言語が生まれたのが1970年代と思うと参照渡しの歴史の深さが伺い知れます。
そしてきちんとした参照渡しは@shiracamusさんが提示してくれた情報処理試験(徹底研究! 情報処理試験)に答えられます。
C言語のポインタは答えられますが非常に間違えやすいので注意が必要です。
(値型の)値渡し
(値型の)参照渡し
参照渡しは常に渡された変数と結び付けられその変更は呼び出し元に反映されます。
参照型の値渡し
参照型の値渡しは少しややこしいですが要は値型はコピーでしか作られないので参照型と切り離されてしまいます、そうすると元の変数(A)の値を変更することはできません。
ポインタと低水準
さて早いですがラスボスの出現です。その前に確認です、ここまでの説明で変数がこのアドレスに格納されていてそれを指しているのがという低水準の話はしていません(よね)。
ポインタとはまさに高水準と低水準の橋渡しをするためのものです。
ここからはコードを出しましょう。
void SubX(int* C, int D) {
int E;
E = C;
C = D;
D = E;
}
int main() {
int A;
int B;
A = 1;
B = 2;
SubX(&A, B);
}
C言語(gcc、GNU Cコンパイラ)だとassignment makes integer from pointer without a cast [-Wint-conversion]
という警告を出しつつもコンパイルが通りました警告の内容はint
をポインタに置き換えてるけど大丈夫?です。
C++(g++、GNU C++コンパイラ)だとinvalid conversion from ‘int’ to ‘int*’ [-fpermissive]
というコンパイルエラーになります。意味はint
をポインタになんかできるか馬鹿!です。
一見、C言語のほうが優しそうですがポインタまわりは罠だらけなのでC++のほうが親切です。
これの何が悪いのかというとポインタ型からint型への変換です。ポインタ型とint型はメモリサイズが同じなのでビット(2進数)まで見ると代入(assign)できますが符号とかいろいろ問題はあります。
なのでintからdoubleに置き換えましょう。
void SubX(double* C, double D) {
double E;
E = C;
C = D;
D = E;
}
int main() {
double A;
double B;
A = 1;
B = 2;
SubX(&A, B);
}
incompatible types when assigning to type ‘double’ from type ‘double *’
エラーです意味はdoubleはポインタ型double*
に変換できませんです。
ポインタ型の危険度を解説するためにだいぶ脇道にそれた感もありますが本論に戻りましょう。
ポインタ型の値渡しで参照渡しと同じことをしようとする(間違い編)
#include <stdio.h>
void SubX(double* C, double D) {
double E;
E = *C;
C = &D;
D = E;
}
int main() {
double A;
double B;
A = 1;
B = 2;
double* ptr_A=&A;
SubX(ptr_A, B);
printf("A=%f B=%f \n", A, B); A=1.000000 B=2.000000
}
double* ptr_A=&A
の部分はAのアドレスをアドレス演算子**&**で取得しdouble型のポインタであるptr_A
に代入しています。関数内に受け渡された時点で別のdouble型のポインタC
にアドレスの値がコピーされます。関数内部でCにDのアドレス値を代入していますが、関数内のCと関数の外にあるptr_Aは別の変数であるためptr_Aが影響を受けることはありません。
AとBは浮動点小数型なので小数値で表されますprintf("A=%f B=%f \n", A, B)
のfは浮動点小数(floating point number)を表しています。floatとdoubleは誰かが解説してくれると信じて先に進みます。
これを絵にすると次のようになります。
ポインタ型の値渡しで参照渡しと同じことをしようとする(正解編)
#include <stdio.h>
void SubX(double* C, double D) {
double E;
E = *C;
*C = D;
D = E;
}
int main() {
double A;
double B;
A = 1;
B = 2;
double* ptr_A=&A;
SubX(ptr_A, B);
printf("A=%f B=%f \n", A, B); // A=2.000000 B=2.000000
}
C=&D
から*C=D
にしました。これはCが**`*``**間接演算子を使ってポイントしている先の変数にアクセスしてその値にDの値を代入しています。Cとptr_Aは同じ変数をポインしているためCのアドレス値を経て呼び出し元のAの値が書き換えられます。図にすると下のようになります。
この間接演算子が定義されているのはC/C++以外で見たことがありません。(C++ではポインタは使うななので実質C言語のみです)
理由は明らかで参照は低水準を隠匿して高水準なものにしか触れられないようにしたのになぜ低水準から変数にアクセスする必要がるのか?です。
低水準にアクセスすることの対価が間違えれることです、私は間違えました、誰かがわざと間違えた構文を公開しないとも限りません。
なので間違いが起こせる構文よりは構文上で間違えられないようにするべきです。
C言語のみで語ることによる誤解
参照渡しの歴史はC言語より古いですがC系に参照が取り入れられたのはC++からで標準化が1998年のC++98で今よく使われているの多くの言語とほぼ同年代です。だからこれらの言語の基礎がC言語の文脈で語られるのはしかたがないことかもしれません。
しかし、C言語には参照渡しの概念がないので値型の値渡しを単に値渡し、ポインタ型の値渡しを単にポインタ渡しと言います。
さらに、C言語には参照型の概念もないのでポインタを参照と読ぶことが多々あります。それはC言語だけで語るなら正しいのかもしれませんが他言語を考慮に入れた時は必ずしも正しいとは言えません。
C言語では大きな構造体は値渡しでコピーを作るのはコストがかかるので構造体はポインタ(型の値)渡しが推奨されています。ポインタ(型の値)渡しではポインタ(int型と同サイズ)のコピーで済みます、ポインタ渡しでもコピーを発生させないわけではありません。
ここで一度C言語から離れて参照型の値渡しをみてみると値の実体自体をコピーしています、これはC言語を(のみ)知っている人にとっては高コストに感じられるのでポインタ(型の値)渡しを使っていると考えるのでしょう。ポインタを参照と呼ぶC言語の事情も相まってポインタ(型の値)渡しが参照(型の値)渡しとなり()を省略して参照渡しなったと考えられます。
更にポインタ型にはポイント先の値を直接代入できる***
間接演算子があるので参照渡しと同じようなことができるのでよりポインタ(型の値)渡しが参照渡しと呼ばれることに拍車をかけているのではないかと訝っています。
他の言語のコンパイラやインタプリンタの内部実装を想像してみるとC言語のポインタを使っていてそれを参照型と読んでいるのかもしれませんがその場合もアドレスの値自身と*
**演算子を隠匿して低水準にアクセスできないようにしてるのが普通だと思います。そもそもC言語で実装されている保証もありません。想像で語るなとはいいませんが想像で語ることには誤解を与える危険性もありますし自分も誤解をしている危険性もあります。
C++での参照渡し
#include <iostream>
void SubX(int &C, int D){
int E;
E=C;
C=D;
D=E;
}
int main(){
int A, B;
A=1;
B=2;
SubX(A, B);
std::cout<<"A="<<A<<" B="<<B<<std::endl; // A=2 B=2
}
最後にC++での参照渡しを提示します。C++での参照型(T&
)は値型に暗黙の型変換がされるので余計な演算子を書く必要がなくプログラマーが間違えにくくなっています。
初心者とC言語と低水準
私は初心者が低水準やC言語は必ずしも触る必要はないと考えています。
では何故こんな記事を書いたのかというと触れる必要はなくとも知っておいてほしいからです。
C言語やC++は高級言語ながら低水準にアクセスできる便利な言語です。今回て提示したコードは注意書きがない限りgcc/g++ -Wall -Wextra ファイル名
で警告なしのコンパイルが可能です、gccはC言語コンパイラ、g++はC++コンパイラです。Linux/Unixならたいてい入っていますし。Windowsでも入れるのはそう難しくないはずです。
今回は説明で端折った部分もあるのでもっと詳しく知りたい場合は元記事を見てもらうか自分で試してみてください。
そしてC/C++はこうした泥臭いことが得意なのでカッコいいGUIアプリや3Dゲーム、Webアプリを作りたいとなると不向きな言語です。