今度こそ、ポインタ編がいったん完結になる。
今回は、前回解説した文字列、配列とポインタの関係をお話しよう。
言語ごとに仕様が異なるが、それぞれの内容を補強するような内容にするつもりだ。
どんな考え方が使われているか、重点的に見ていく。
今回の説明内容
今回はポインタ後編、前回解説した文字列と配列が、ポインタに絡んでくる部分の解説を行う。
前半では、配列についてお話しよう。ちょっと面白い実験的なものもしてみる。
また、C言語での文字列連結についても解説しよう。結構複雑なので、ゆっくり読んで欲しい。
後半では、主にJavaの内容になってしまうが、文字列…というか、もうちょっと発展させたプリミティブ型、参照型という考え方をご紹介する。
このあたりはのちの関数、オブジェクトにも絡んでくるので、その時にも再度触れるつもりだ。
配列とポインタ
まずは配列とポインタの関係から。
配列の復習
配列とは、多数のデータを一つの変数名と添え字によって扱う考え方だった。
宣言時には要素を直接指定する、あるいは要素数を決めておき、変数名[添え字]の形でアクセスすることができた。
で、C言語の文字列のところで、配列とポインタが同じものを表すとさらっと書いたのだが…その部分を深く掘り下げていく。
配列の実体
結論から言ってしまおう。配列の実体は、ポインタだ。
配列を宣言するときに、JavaやC言語だと要素数を決めていた。要素を直接定義する場合でも、その数は決まる。
で、内部では、この数だけ連続した住所を確保している。
実は、この連続して確保した住所の先頭へのポインタを持っているのが、配列の変数だ。
それに添え字の数だけずらした中身を持ってくる、という考え方になる。
なぜJavaやC言語だと配列の要素数を途中で増やせないかというと、事前に確保した範囲外になってしまうからだ。
そのため、途中で増やす場合はその分を改めて確保しなければいけない。そのため、別途処理が必要になる。
とはいえ、できるだけ最初に必要な領域は確保しておくようにしよう。
Javaのサンプル
Java言語で見てみよう。
まず、配列用変数arr1
を普通に用意する。で、その中に数字を入れておこう。
その後、別の配列用変数arr2
を用意して、これにarr1
を直接代入してみる。
この処理部分を書くと以下のような感じだ。
int arr1[] = {1, 2, 3};
int arr2[] = arr1;
この状態でarr1
の内容を出力すると、もちろんそのまま1, 2, 3
となる。
では、この時にarr2
の内容を変更してみよう。例えば、arr2[0] = 10;
といった形。
最終的には、以下のようなソースコードだ。
class Sample10 {
public static void main(String args[]) {
int arr1[] = {1, 2, 3};
int arr2[] = arr1;
System.out.println("arr1の一番目の要素:" + arr1[0]);
System.out.println("arr1の二番目の要素:" + arr1[1]);
System.out.println("arr1の三番目の要素:" + arr1[2]);
arr2[0] = 10;
System.out.println("arr1の一番目の要素:" + arr1[0]);
System.out.println("arr1の二番目の要素:" + arr1[1]);
System.out.println("arr1の三番目の要素:" + arr1[2]);
}
}
これで、どのように表示されるか。
…ちょっと分かりづらいと思うので、表にしてみよう。4行目実行時点で見てみる。
まずは、変数とその中の値。
変数 | データ |
---|---|
arr1 | 100番地 |
arr2 | 100番地 |
arr2
には、arr1
の内容をそのまま代入しているので、こうなる。
で、その番地には何が入っているかというと…
住所 | データ |
---|---|
100番地 | 1 |
101番地 | 2 |
102番地 | 3 |
こんな感じだ。
例を出してみよう。arr1[0]
を表示するときは、arr1
の指す場所から0個ずらすので、100+0で100番地のデータを持ってくる。
つまり、1が持ってこれる、というわけだ。他のarr1[1]
、arr1[2]
も同様。
では、ここでソースコード9行目のarr2[0] = 10;
を実行するとどうなるか。同じように見ていこう。
arr2[0]
はarr2
の場所から0個ずらすので、100+0で100番地のデータを参照する。
そこに、10を代入しているので…
住所 | データ |
---|---|
100番地 | |
101番地 | 2 |
102番地 | 3 |
こうなる。
ここで、arr1
の内容をもう一回表示してみよう。
アクセスの流れはもう3回目なので省略するが、arr1[0]
を表示するときは100番地を見ていた。
で、今見ると…値が10になっている。
つまり、表示すると10, 2, 3
となるというわけだ。
ポインタの考えが使われているのが分かるだろうか。
このあたりの動作はC言語はもちろん、JavaScriptでも同じになる。
表面上には見えないので厄介だが、気を付けないとドツボに嵌るので注意しよう。
C言語の文字列とポインタ
C言語の場合の文字列について、もう少し詳しく見ていこう。
前回、C言語の文字列は、char型(1文字)の集まりだということを書いたと思う。
では、具体的にどうなっているのか。ちょっと表も併せて見てみよう。
char str[] = "Hello";
こう書くことで、strにはHello
という文字列が入る。
これが具体的にどういうことかというと、str
が100番地を指しているとして、以下のようになっている。
番地 | 100 | 101 | 102 | 103 | 104 | 105 |
値 | H | e | l | l | o | \0 |
1つの領域ごとに1文字が格納されており、最後に\0
というデータが入っている。
これは特殊文字(エスケープシーケンス)の一つで、文字列がそこで終わりであることを表している。
そして、前回書いた通り、配列は最初の段階で要素数が決定する。
つまり、今回の例はH, e, l, l, o, \0
という6個のデータを持つchar型の配列として用意されていることになる。
これが、C言語の文字列の正体だ。
では、二つの文字列を連結することを考えてみよう。
まず、使用する処理を紹介する。
strcat(文字列変数, 文字列);
こう書くと、一つ目の変数の後ろに、二つ目の文字列+\0
をくっつけることができる。
このとき、一つ目の文字列の末尾にある\0
の個所から、二文字目が始まる。
このstrcat
という処理は、string.hというものの中に定義されている。
そのため、これを使用する際はファイルの先頭の方で#include<string.h>
という記述をしてあげよう。
具体例を出してみよう。
str1
に元々Hello
という文字列が入っており、後ろにWorld
という文字列が入ったstr2
をくっつけてみる。
ソースコードに直すと以下のようになる。確認用で最後にstr1
を出力しておこう。
char str1[] = "Hello";
char str2[] = "World";
strcat(str1, str2);
printf("%s\n", str1);
これを実行すると、上手くいけばHelloWorld
と表示される。
…のだが、実はこれをやってしまうと問題が発生してしまう。
何がまずいのか。状況を整理してみよう。
str1
には、Hello
と後ろに\0
、計6個の要素が入っている。str1
の要素数は6、つまり、事前に6個の領域が確保されている。str2
には、World
と後ろに\0
、計6個の要素が入っている。str1
にstr2
をくっつけると、6 – 1 + 6 = 11個の領域が必要になる。
C言語では、配列を定義した際に領域の数が決まる、と説明したと思う。
今回で言えば、str1
の領域は6個だ。
ここに、文字列を足してしまうと…その領域からあふれてしまう。
つまり、本来は使えない場所まで使ってしまうのだ。
このように本来使ってはいけない領域まで使ってしまうことをオーバーランと呼ぶ。
例えば、100番地からstr1
を用意したとすると、この文字列は100番地から105番地まで使用されている。
ここで、別のint型変数が、例えば106番地に用意されていたとしよう。以下のような感じだ。
番地 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
値 | H | e | l | l | o | \0 | 100 |
ここで、別の文字列World
をstr1
にくっつけてみる。
すると、105番地の\0
から書いていくので…106番地の数字が消えてしまう。
こんなことになってしまうと、プログラムが上手く動いてくれない。
だから、あらかじめ文字列をくっつけることを見越した要素数を定義しておかなければいけないのだ。
今回の場合、最終的には11個の領域が必要になる。だから…
char str1[11] = "Hello";
char str2[] = "World";
strcat(str1, str2);
printf("%s\n", str1);
こんなふうに、先に11個分の領域を確保してあげれば、今回のオーバーラン問題を回避することができる。
もう一つ処理をご紹介しよう。
strncat(文字列変数, 文字列, 文字列のサイズ)
これもやっていることはstrcat
と同じだが、少し処理が加わる。
文字列変数に対して、何文字くっつけるかを指定することができるのだ。
三つ目に指定した数字の数だけ文字を足し、さらに後ろに\0
をくっつけることになる。
足そうとしている文字列の長さより指定したサイズの方が小さければ、そこで切られて、最後に\0
がくっつく。
例えば、str1
で11個分の領域を確保しておき、Hello
という文字列を入れておく。
で、str2
にWorld!!!
という文字列を入れて、これらをくっつけよう。
このとき、str1
には元々6文字分使用されており、str2
には9文字分使用されている。
そのまま全部くっつけようとすると、str1
の\0
を除いた5個+str2
の9個で計14個の領域が必要となってしまう。
str1
に足せる文字列は、11-(6-1)-1=5文字である。つまり、これを指定してあげるのだ。
この計算式は、まず11が全体の確保した領域、6-1がくっつける際に元々使用される文字列、つまり11から引くことで、残りの空いている領域数を出している。
そして、終端に1個、\0
というデータがくっつくので、空き領域から1を減らした文字数だけ新たに足すことができるのだ。
領域数は変数宣言時に定義しているので、それとは別で文字列の長さを計算できれば、足すことのできる文字数を計算で出せる。
その、文字列の長さを求める処理が以下のものだ。
strlen(文字列)
こうすることで、文字列の入った配列の長さではなく、文字列の長さを持ってこれる。
これには最後の\0
は含まれていないので、上の計算式の6-1の部分がこれになる。
ここまでの内容を踏まえて、二つの文字列を安全に足す処理を書いてみよう。
#include<stdio.h>
#include<string.h>
int main(){
char str1[11] = "Hello";
char str2[] = "World!!!";
int str1_length = strlen(str1);
int addable_length = 11 - str1_length - 1;
strncat(str1, str2, addable_length);
printf("%s\n", str1);
return 0;
}
これで安全に足すことができた。
結果としては、addable_length
は今回5となるので、str2
の5文字分…World
だけ足されるので、HelloWorld
と表示される。
オマケ:define(マクロ)
…安全に足すことはできたのだが、str1
の領域を変えようとした場合に、2か所直す必要がある。
これでは直し忘れが怖いので、一か所で定義しておきたい。
そこで、その領域数も変数に…できればよかったのだが。
実は、C言語では配列の要素数を定義する際に変数を使うことができない。
そこで使えるのが、defineというものだ。マクロと呼ぶ。
これはincludeと同じ場所に書き、そのファイル内で使う一定のデータを定義することができるようになる。
書き方は以下の通り。
#define マクロ名 データ
こうすることで、このマクロ名を書いたところにデータの内容が置き換わる。
最初一回しか代入できない変数のようなイメージでいいだろう。
通常、マクロ名は全部大文字で使われる。
これを使って、str1
の要素数を定義しなおしたものが以下だ。
#include<stdio.h>
#include<string.h>
#define SIZE 11
int main(){
char str1[SIZE] = "Hello";
char str2[] = "World!!!";
int str1_length = strlen(str1);
int addable_length = SIZE - str1_length - 1;
strncat(str1, str2, addable_length);
printf("%s\n", str1);
return 0;
}
これでstr1
の要素数を増減させるのが楽になった。
…かなりC言語特化の内容になってしまったが、このようなサイズの考え方は非常に重要となる。
どの言語でも、しっかりと意識しておいて欲しい。
Java:プリミティブ型と参照型
このタイミングで解説するということは…勘のいい方ならお気づきだろうが、これもポインタの考え方に絡んでくる。
Java特化の内容だが、これも重要な考え方なので解説してしまう。
プリミティブ型とは
プリミティブ型とは、変数へ代入した際に、その中にそのままその値が入る型のことだ。
例を挙げると、これまでにも出てきた整数を表すint型や真偽値を表すboolean型、他にも1文字を表すchar型、小数点以下も表現できる浮動小数点float型など。
共通点は、型名の先頭が小文字で始まること。つまり、小文字で始まる型はプリミティブ型と思ってもらって構わない。
参照型とは
まあそのままなのだが…
参照型とは、変数の中にそのデータへの参照…つまり、ポインタが格納される型のことだ。
例を挙げると、文字列を表すString型、各型の配列など。
基本的にプリミティブ型以外は全て参照型と思ってくれて構わない。
使用時の注意と補足
これなのだが…今のところ解説できるのは、今回の最初の方でお見せした配列の代入くらいだ。
配列をそのまま別の配列へ代入しても、結局は参照のみが渡されるので、その中身を変更すると代入元まで変わってしまう。
これと、一つ補足がある。文字列の比較だ。
真偽値のところで、Javaの文字列比較は以下で行うと書いた。
文字列1.equals(文字列2)
なぜこれを使用するかなのだが、これはStringが参照型であることに関係している。
そのままイコール二つで比較する場合、変数に入っているもの同士を比較することになる。
そして、文字列であるString型は参照型だ。ということは、変数に入っているのはそこへのポインタ。
つまり、イコール二つでは、その変数の値がどこに入っているかで比較してしまうのだ。
これでは、別の場所に全く同じ文字列が入っていた場合にもfalse
となってしまう。
そこで、中に入っている文字が同じかどうか、という処理が別で用意されているのだ。
それが、上で紹介したequals
という処理。
なので、文字列比較を行う際にはこちらを使おうという補足だ。
まとめ:ポインタと文字列
今回はC言語、Javaに特化した内容が多くなってしまった。
前半では、配列が実際にどうなっているかを解説した。
どの言語でも、配列は先頭へのポインタが渡されており、添え字で実際のデータにアクセスしているということを見ていた。
また、C言語における文字列の扱い方も解説した。この要素数の考え方はきっちりと理解しておこう。
後半では、Javaに特化して、型には2種類あるということを見てきた。
変数に値が直接入るプリミティブ型、値への参照が入る参照型があるよということを意識しておいて欲しい。
このあたりの話が具体的になるのが、次回の関数、そして次々回のオブジェクトになる。
基礎から、ちょっと踏み込んだ使い方まで書こうと思うので、今回までのポインタの考え方をしっかり復習しておこう。
更新情報はTwitterでも呟いている。よかったら、ページ下部のTwitterアイコンから覗いていってほしい。
それでは。
コメント