C言語 ポインタについて学んだことを書き起こす その①

初投稿です。

「学んだことはブログなどに書き込んでアウトプットすると良い」

らしいので試してみます。

 

今回は、C言語のポインタについて学んだことをメモ兼アウトプットします。

学ぶのに使った本は「C言語ポインタ完全制覇(ISBN978-4-7741-9381-6)」

という本です。

 

今回は第一章の内容をメモしていきます。

 

ポインタ型とは

例えば「int型」や「double型」が存在するが、それらから派生して作り出される型。

ポインタ型にさせるためには、型に「*」を付け加える。

そうすることで「intへのポインタ型」「doubleへのポインタ型」へと派生される。

ちなみに*は「間接演算子」と呼ぶ。

 

int iHoge;  //int型
int* pHoge; //intへのポインタ型

 

で、このポインタ型には値として、メモリのアドレスを入れることができる。

 

例えば、iHogeのアドレスをpHogeに入れたいときには、変数に「&」を付け加える。

&は「アドレス演算子」と呼び、変数のアドレスを表示させるためのもの。

iHoge = 10;
pHoge = &iHoge; //iHogeのアドレスを、pHogeに代入

↓ このようにしたときのイメージ

ポインタのイメージ

このときの状況は、「pHogeは、iHogeを指している」と言うらしい。

 

次のように、ポインタ型に対して*をつけることで

指した先の値を表示することができる。

printf("%p",(void*)pHoge); //「0x01234」が表示される
printf("%d",*pHoge); //「10」が表示される

 

添字演算子

配列の要素を参照する [ ] を「添字演算子」と呼ぶ。

 

ちなみに、宣言のときの *[ ] と、式の中に現れる *[ ] は全く別もの。

と、本の筆者は述べてました。

この添字演算子も、ポインタと似たような性質を持っていますが

詳細は後で書きます。

 

ポインタ演算

C言語には、ポインタ演算という独特な機能がある。

 

次のようにしたとき、例えば「0x01234」が出力されるとする

int iHoge = 10;
int* pHoge = &iHoge; //iHogeのアドレスを、pHogeに代入

printf("%p\n",(void*)pHoge);

 

次に、pHogeに対して1加算すると、型のサイズ分だけアドレスが増加する。

「0x01234」の4バイト先なので、出力は「0x01238」になる。

※今回の例ではint型は4バイトとする

iHoge += 1; 
printf("%p\n",(void*)pHoge);

 

ヌルポインタ

「ヌルポインタ」とは何も指していないことが保証されるポインタのことを言う。

ちなみに一昔前のネットミーム「ぬるぽ」もここから来てるんだとか。

 

・・・話を元に戻して

ヌルポインタを表す定数値として、通常はマクロNULLが使用されるとのこと。

自分の環境のNULLは次のようになってました。

よく見る「NULL」の正体は「(void*)0」でした。

この0という数字は、「ゼロ番地」的な意味合いになるみたいです。

 

C言語では、0という定数は、ポインタとして使う場合ではヌルポインタとして扱われる。

とのことです。

なので次のように、ポインタ型に対して0を代入することができます。

ただし0以外の値はただのint型になるので、環境によっては警告が出ます。

*hoge = 0; // コンパイラがNULLポインタとして読み替える。
*hoge = 3; // 環境によっては警告が出る

 

ただ「単に定数0を渡しているプログラムは移植性が低い」と筆者は述べてました。

ヌルポインタを表現したいときは0ではなくNULLを代入すべき、と解釈しました。

 

ヌル(NULL)とナル(\0)の違い

・ヌルポインタ

 ポインタ変数が、有効なメモリ領域を指し示していない場合に使用される。

  

・ナル

 文字列の終端を示す時に使用される。

 

よくある間違いとして、文字列の終端させるためにNULLを使うのは間違い。

hoge[len] = NULL; //間違い

 

配列

次の「例1」と「例2」では、同じことをしている。

int iHoge[5] = { 0 , 1 , 2 , 3 , 4}
int* pointer;
int i;

// 例1
for(i = 0; i < 5; i++) {
printf("%d\n", *( p + i ));
}

// 例2
for(i = 0; i < 5; i++) {
printf("%d\n", p[i] );
}

 

どちらも出力は以下のようになる。

0
1
2
3
4

 

まず、添字演算子 [ ] は、宣言と式では全く異なるもの

という前提があるらしい。

 

  • 宣言で使う添字演算子 [ ] は、これが「配列である」ということを表す。
  • 式の中で使う添字演算子 [ ] は、例1にある *(  p + i  ) の簡略記法。

 

という具合。

なので、ポインタ型変数 pointerに配列の先頭アドレスを渡すとき

p = &array[0]

p = array

は、どちらもやっていることは変わらない。

つまり、式の中での

p[ i ]

これは

*( p + i )

を略したもの、ということになるらしい。

シンタックスシュガー。

 

またC言語においては、関数の引数として配列を渡すことができない。

一見配列を渡しているように見えるプログラムでも

実際は、配列の先頭アドレスを渡しているだけになる。

(ということは、引数には配列の要素数も必ず渡さなければ危険ということか・・・)

 

つまりは、次のような関数を作ったとしても

int func1( int a[] )
int func2( int a[10] )

結局値として渡されるのは配列の先頭アドレスなので

int func3( int* a )

このような書き方をしているのと全く一緒。コンパイルによって書き換えられるらしい。

個人的に思ったのは、func1、func2の書き方は、引数として配列を渡してほしい時とか

特定の要素数だけの配列を渡してほしいときに、あえて書く時ぐらいかな?

 

その②に続きます。