アキヒロの日誌

新社会人一年目の記録

c言語学習2019/1/4

 
苦しんで覚えるc言語から一部引用しています。
 

 

苦しんで覚えるC言語

苦しんで覚えるC言語

 

 

気づきやメモ
 
15章
改めて学んだこと
  • メモリ上の番号を表示する方法
  • ポインタという単語
  • ポインタの役割
  • 他言語のポインタ
 
 
 
 
 
  • メモリ上の番号を表示する方法
 
実は、変数につけられた番号は、プログラムで調べることが出来るのです。
 
番号を調べるのは意外に簡単で、printf関数で%p指定子を使うだけです。
ただし、変数名の前には、&をつける必要があります
次のプログラムは、int型変数iの番号を表示する例です。

#include
 
int main(void){
int i;
printf("%p\n",&i);
return 0;
}
このプログラムの実行結果は、次のようになるかもしれません。

0012FF80
これは筆者の環境で実行した結果ですが、結果の数字は、
パソコンや使用するコンパイラによって異なります。
指定された変数をどの番号のメモリに割り当てるのかは、
コンパイラ(正確にはリンカ)が自動的に決めてしまい、
更にその値もOSの仮想メモリ機能によって変化するからです。
 
 
  • ポインタという単語
1つ目は、ポインタ型です。
今まで出てきたint型やdouble型と同じような型です。
ただし、ポインタ型の場合、それらとは少し異なる特徴があります。
 
2つ目は、ポインタ値です。
これは、ポインタ型で扱える数値、要するにアドレスのことです。
整数や実数といった数値の区別と同様に、ポインタ値という区別があるのです。
 
3つ目は、ポインタ変数です。
これは、ポインタ型で宣言された、ポインタ値を記憶出来る変数のことです。
int型の変数やdouble型の変数と基本的には同じことです。
 
 
①ポインタ型
ポインタ型は、要するにアドレスを記憶する変数の型
他の型とポインタ型を合体させて作ります。
例: int型とポインタ型を合体させると、intへのポインタ型という型が出来ます。
double型の場合なら、doubleへのポインタ型が作り出されます。
また、intへのポインタ型に更にポインタ型を合体させて、
intへのポインタのポインタ型という、多重のポインタ型を作ることすら出来ます。
合体させる理由:
指定アドレスに記憶された数値を取り出すため
ポインタ型は、どんな型の変数のアドレスだったかわかる必要があります。
そこで、あらかじめ他の変数と合体した形でポインタ型として作っておけば、
そのポインタ型の変数に記憶された数値は、合体されている型であるとすぐわかります。
 
②ポインタ値
変数のアドレスの値
 
疑問: アドレスが単なる整数値であるなら、int型に記憶すれば事足りるはずなのに、何故、わざわざポインタ値などという新しい数値として扱う必要があるのでしょうか?
解答:ポインタ地と整数値ではその意味が違い、目的が全く別である。
通常の整数値は、プログラムの中で計算などを行うための数値ですが、
ポインタ値は、計算に使われる数値ではない。
結論:int型の変数で両方を扱っても、何のメリットもありません。むしろ、変数に記憶される数値が整数値なのかポインタ値なのかわかりにくくなります。であれば、いっそのこと、2つを別々の数値とした方がよほど便利になります。
 
 
③ポインタ変数
ポインタ型で宣言された実際の変数のこと。 この変数には、その元となった型の変数のアドレスを自由に代入できます。更に、記憶しているアドレスのメモリを読んだり書き換えたりすることが出来ます。
ポインタ変数の役目:
それが指し示しているアドレス番号のメモリの値を計算することです。
ポインタ変数そのものを計算に使うのではなく、それが指し示している変数を計算するのが、ポインタ変数の目的です。言い換えると、普段はポインタ変数として振る舞っているのですが、
指し示している変数の計算が必要な時には、普通の変数に変身する必要があるのです。
 
2つのモード:
ポインタ変数モードでは、たいした機能は備えていません。
具体的には、アドレスへの代入と足し算引き算だけしかありません。
何故なら、ポインタ変数モードに必要なのはアドレスの記憶だけだからです。
アドレスさえ記憶していれば、後は特に何もする必要がありません。
 
通常変数モードに切り替わった場合、その性質は通常変数と全く同じになります。
おかげで、通常の変数と同様に様々な演算子を使って計算することが出来ます。
当然、その時に使われるメモリは、ポインタ変数モードで記憶したアドレスになります。
 
 
 
 
  • ポインタの役割
疑問点: ポインタとは何の役に立つ機能なのか????
 
解答: ポインタの本当の使い方は、ショートカットとして使用することです。
Windowsのデスクトップに並んでいる、あのショートカットと同じです。
 
ショートカットは、どこか別の場所にあるファイルを指し示すファイルです。
ショートカットを開けば、その指し示しているファイルが開かれます。
にも関わらず、ショートカットは指し示すファイル自体ではないので、
ショートカットはどこにでも自由に作ることが出来ますし、
複数個作ったり削除したりしても、指し示すファイルには何の影響もありません。
 
これこそが、まさにポインタの役割そのものです。
ポインタ変数に、実際に存在する変数のアドレスを記憶しておけば、
そのポインタ変数が使える場所であれば、元の変数が使えない場所(→それぞれどこですか???)であっても、
ポインタ変数を通常変数モードに切り替えれば、元の変数と同じく使うことが出来ます。
まさに、ショートカットのような働きをさせることが出来るわけです。
 
 
 
 
 
  • 他言語のポインタ
他言語のポインタ

一般には、ポインタはC言語C++のみの機能だと言われています。 確かに、指定したメモリのアドレスを操作するという意味ではその通りです。 しかし、ポインタの本当の使い方はショートカットとして使うことであり、 その観点ならば、実用的なほとんどの言語にポインタがありますJavaの参照はまさしくそんな機能で、しかも頻繁に使われますし、 VisualBasicのSETステートメントなども同様と言って良いでしょう。 そもそも、ポインタがないのでは、連結リストや木構造などの、 複雑なデータ構造を実現出来ませんし、オブジェクト指向も困難です。 その意味では、仕組みが不明なJavaVisualBasicのポインタより、 仕組みがはっきりしているC言語のポインタの方が理解しやすいです。 他の言語の参照と、C言語のポインタの最大の違いは、自動なのか手動なのか、です。 他の言語の参照は、ほとんど自動でショートカットとして機能するようになっていますが、 C言語のポインタは、完全に手動であり、プログラマーが完全に理解して使わなければなりません。 そのかわり、上級者がC言語のポインタを使いこなすと、ポインタだけで、 ほぼあらゆる制御構造、あらゆるデータ構造、を実現可能な強力すぎる機能となります。 実際、C言語の様々な機能って、ほとんどポインタで成り立ってるといっても過言ではありませんし
 
 
 
 
 
 
 
 
 
 
 
 
 
15章の後半で学んだこと
  • ポインタ型の引数
  • 配列型の引数
①ポインタ型の引数
void func(int *pvalue);
int main(void)
{
int value = 10;
printf("&value=%p\n",&value);
func(&value);
printf("value=%d\n",value);
return 0;
}
void func(int *pvalue)
{
printf("pvalue=%p\n",pvalue);
*pvalue = 100;
return;
}
②配列型の引数
double getaverage(double data[10]);
int main(void)
{
double average,array[10] = { 1,2,3,4,5,6,7,8,9,10 };
average = getaverage(array);
printf("%f\n",average);
return 0;
}
double getaverage(double data[10])
{
int i;
double sum = 0;
for (i = 0; i < 10; i++)
{
sum += data[i];
}
return sum / 10;
}
double getaverage(double data[10]);
int main(void)
{
double average, array[5] = { 1,2,3,4,5 };
average = getaverage(array);
printf("%f\n", average);
return 0;
}
double getaverage(double data[10])
{
int i;
double sum = 0;
for (i = 0; i < 10; i++)
{
sum += data[i];
}
return sum / 10;
}
おかしな結果となった
引数の型は10要素になっているにもかかわらず、5個しか要素のない配列が渡せる。
その結果、関数側では強引に10個の要素を処理してしまい、おかしな結果となっています.
→要素数は無視されたという結果となった
double getaverage(double data[10]);
int main(void)
{
double average, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
average = getaverage(array);
printf("%f\n", average);
printf("%f\n",array[3]);
return 0;
}
double getaverage(double data[10])
{
int i;
double sum = 0;
for (i = 0; i < 10; i++)
{
sum += data[i];
}
data[3] = 12;
return sum / 10;
}
→呼び出された関数の中で引数の値を変更すると、呼び出し元の引数の値を変更できた。
double getaverage(double data);
int main(void)
{
double average, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
average = getaverage(array);
printf("%f\n", average);
printf("%f\n",array[3]);
return 0;
}
double getaverage(double data)
{
int i;
double sum = 0;
for (i = 0; i < 10; i++)
{
sum += data[i];
}
data[3] = 12;
return sum / 10;
}
→配列の宣言時に要素数を書かなくても実行された
→要素数は無視されている
素数を書かない状態でも、呼び出し元の引数(array[3])の値を変更することが出来た
→ポインタ型の引数を使った時と似ているのでは???
少し実験をする↓↓↓
double getaverage(double *data);
int main(void)
{
double average, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
average = getaverage(array);
printf("%f\n", average);
printf("%f\n", array[3]);
return 0;
}
double getaverage(double *data)
{
int i;
double sum = 0;
for (i = 0; i < 10; i++)
{
sum += data[i];
}
data[3] = 12;
return sum / 10;
}
問題なく実行され1つ上のプログラムと同じ結果を得た
→配列要素をコピーして関数に渡していたのではない
→→配列の先頭のアドレスを渡していたことになる
つまり配列の先頭アドレスを渡すだけなら、要素数は関係ないので、上記のように関数宣言時に要素数が無視されることになる。
まとめると、下の①②③は同じ意味の仮引数宣言になる。
①int getaverage(int data[10]);
②int getaverage(int data)
③int getaverage(int *data);
ただし、この3つが同じ意味になるのは関数の仮引数宣言の場合のみ。
おかしなところはないだろうか??上記のプログラムをもう一度見てみる。
double getaverage(double *data);
int main(void)
{
double average, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
average = getaverage(array);
printf("%f\n", average);
printf("%f\n", array[3]);
return 0;
}
double getaverage(double *data)
{
int i;
double sum = 0;
for (i = 0; i < 10; i++)
{
sum += data[i];//dataはポインタ変数なのに配列として要素を指定して計算している
}
data[3] = 12;
return sum / 10;
}
苦Cより説明すると↓↓
「数式の中に配列名を記述した場合、の記号の有無にかかわらず、
配列名は、配列の先頭要素へのアドレス(ポインタ値)として扱われます。
そして、その配列名にをつけた場合、そのアドレスに番号の値だけ足し算を行い、
その結果として、足し算された分の番号の要素として扱われているのです。」ということ。
→「ポインタ変数」を「配列」のように使うことが出来る
ポインタ変数を配列のように使用するプログラムを作ってみる
int main(void)
{
int *data;
int i, average = 0, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
data = array;//ポインタ変数に配列を代入する
for (i = 0; i < 10; i++)
{
average += data[i];//ポインタ変数を配列のように使っている
}
printf("%d\n",average/10);
return 0;
}
→ポインタ変数に配列を代入すると、ポインタ変数を配列のように使うことが出来る=演算子で要素番号の指定が出来るという意味
→動的メモリ確保をする時に必要になるプログラム
普段は使わない
[]演算子で要素番号の指定する以外にも他の方法がある
実はポインタ変数用の書き方がある
int main(void)
{
int *data;
int i, average = 0, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
data = array;//ポインタ変数に配列を代入する
for (i = 0; i < 10; i++)
{
average += *(data+i);//ポインタ演算!!(他の方法)
}
printf("%d\n", average / 10);
return 0;
}
こんな書き方もある
int main(void)
{
int *data;
int average = 0, array[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (data = array; data!= &array[10]; data++/*++ポインタ演算は高速な処理が出来る*/)
{
average += *data;
}
printf("%d\n", average / 10);
return 0;
}
組み込み系の話
ほとんどのパソコン向けのコンパイラは適切な最適化を行ってくれます。
さらに、パソコン用のCPUは内部のキャッシュの仕組みが複雑かつ高性能であり、
繰り返し処理をCPUが独自に最適化して、速度を向上するような仕組みがあります。
そのため、現代的なパソコンでは、どちらの書き方でも、結局同じ速度になります。
しかし、組み込み(家電などに内蔵される低性能なコンピュータ)ではそうとは限りません。
コンパイラによる最適化は十分には機能しないことも多いですし、
CPUの仕組みが単純なので、プログラムの書き方が、速度にそのまま反映されやすくなっています。
そういった場合には、ポインタ演算は現代でも有用です。