Comments
Description
Transcript
プログラミングC 第5回 ポインタ(2) -- ポインタ演算 配列の
Prog-C 2016 Lec05-3 } effff9c8 std0dc0{s1000001}2: 実行結果 std0dc0{s1000001}1: 0 effff9c8 11 1 effff9cc 22 2 effff9d0 33 3 effff9d4 44 アドレスは環境 によって違うが、 要素のアドレス の差に注目 ./a.out Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-1int.c for(i = 0 ; i < 4 ; i++) printf( "%d %p %d\n",i,&array[i],array[i]); printf("\n%p\n",array); return 0; #include <stdio.h> int main() { int i; int array[] = {11,22,33,44}; Prog-C 2016 Lec05-4 SはSolarisを、Mは Macの場合を表す 4 8 int,float,long(S) ポインタ(S) double,long(M), ポインタ(M) 2 1 char short 大きさ (バイト数) 型 Copyright (C) 1999 - 2016 by Programming-C Group イメージ 文字型とint型で分かる通り、配列の各要素は型の大きさ分だけアドレス が離れている。 各型の大きさを再掲する この値は会津大学の標準的な環境での値である Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-1char.c 前頁の例は、文字型の配列であったが、他の型の配列も同じよう に考えられる。 int型の場合は以下のようになる Prog-C 2016 Lec05-2 effff9d0 std0dc0{s1000001}2: 実行結果: std0dc0{s1000001}1: ./a.out 0 effff9d0 u アドレスは環境 1 effff9d1 によって違うが、 2 effff9d2 a 要素のアドレス の差に注目 3 effff9d3 i 4 effff9d4 z 5 effff9d5 u アドレスの飛び方 Copyright (C) 1999 - 2016 by Programming-C Group } for( i = 0 ; i < 6 ; i++) printf("%d %p %c\n",i, &str[i],str[i]); printf("\n%p\n",str); return 0; #include <stdio.h> int main() { int i; char str[] = "u-aizu"; 配列のアドレスはそれぞれの要素の前に「&」を付加することで知ることが出来 る。例えば、配列strの要素1のアドレスは&str[1]である。 配列名自体の値も出力してみた。配列名の値は最初の要素のアドレスと一緒で ある。(これは後ほど詳しく述べる) 文字型の場合は以下のようになる これまで単体の変数でアドレスを考えてきたが、配列の場合はどうであろうか? 配列のアドレス Prog-C 2016 Lec05-1 下に置いてありますから、各自自分のディレクトリに コピーして、コンパイル・実行してみてください マークのあるサンプルプログラムは /home/course/prog1/public_html/2016/lec/source/ 配列のアドレス(2) ポインタ演算(6)(前期教科書P277~) ポインタと配列(9)(同上P281) 文字列定数と文字列配列(18)(同上P285) プログラミングC 第5回 ポインタ(2) -- ポインタ演算 配列のアドレス 配列の各要素は型の 大きさ分だけアドレス が離れている(添字0 の要素が一番アドレ スが低い) 高 アドレス 低 ポインタ演算例 char型(1バイト) Copyright (C) 1999 - 2016 by Programming-C Group 高 アドレス 低 Prog-C 2016 Lec05-7 配列a 1 2 3 4 Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-2.c p 実行結果 std0dc0{s1000001}1: ./a.out 1 2 3 4 1 2 3 4 1 2 3 4 std0dc0{s1000001}2: for(i = 0 ; i < 4 ; i++) printf("%d ",a[i]); printf("\n"); for(i = 0 ; i < 4 ; i++) printf("%d ",*(p + i)); printf("\n"); for(q = p ; q < p + 4 ; q++) printf("%d ",*q); printf("\n"); return 0; p = &a[0]; int main() { int i , a[] = {1,2,3,4}; int *p ,*q ; #include <stdio.h> } 要素2 要素1 要素0 int型(4バイト) ポインタ演算を利用すると以下のようなプログラムを書くことが出来る Prog-C 2016 Lec05-5 要素2 要素1 要素0 イメージ的には以下のようになる アドレスの飛び方 a[2] a[1] a[0] 要素値(直接) *(p+1) *p *(p-1) 要素値(間接) 文字列操作の例 Copyright (C) 1999 - 2016 by Programming-C Group 実行結果 int main() std0dc0{s1000001}1: ./a.out a i z u \o { aizu int i; aizu char str[] = "aizu"; aizu pは&str[0] char *p ,*q ; uzia 正順出力 (配列の先頭) uzia を指す p = &str[0]; std0dc0{s1000001}2: for(i = 0 ; str[i] != '\0' ; i++) printf("%c",str[i]); printf("\n"); for(i = 0 ; *(p + i) != '\0' ; i++) printf("%c",*(p + i)); printf("\n"); for(q = p ; *q != '\0' ; q++) printf("%c",*q); printf("\n"); a i z u \o 逆順出力 pにヌル文字のア ループ終了時には ドレスを代入 p = q; qはヌル文字のアドレス q が入っている for(i = 1 ; p - i >= &str[0] ; i++) printf("%c",*(p - i)); printf("\n"); for(q = p - 1 ; q >= &str[0] ; q--) printf("%c",*q); printf("\n"); return 0; /home/course/prog1/public_html/2016/lec/source/lec05-3.c }Prog-C 2016 Lec05-8 Copyright (C) 1999 - 2016 by Programming-C Group #include <stdio.h> ポインタ演算例 &a[2] p+1 Prog-C 2016 Lec05-6 &a[1] &a[0] p-1 p アドレス ポインタ int a[10]において、int型のポインタpの値が2番目の要素(要素1)のア ドレス、つまりp = &a[1]だとすると、ポインタ加減算の値は以下のように なる ポインタに1を加える(減じる)と、ポインタが現在保持するアドレス に型の大きさ(int型なら4)を加え(減じ)られる。 ポインタがある配列の要素を指しているとすると、ポインタに1を加 える(減じる)と、一つ後の(前の)要素を指す ポインタ演算(加算・減算)を以下のように取り決める ポインタに対する演算は普通の変数に対する演算と同じように行なう事 が出来る。 ポインタ演算 配列とポインタの相違点 Copyright (C) 1999 - 2016 by Programming-C Group Prog-C 2016 Lec05-11 Copyright (C) 1999 - 2016 by Programming-C Group ポインタは同型の変数や、配列をどれを指してもかまわないが、配列名 は指定されているメモリ領域しか指すことができず、その値を変更する ことはできない。つまり、配列名は、アドレス定数である。 ポインタは適切に初期化しない限り配列の代用にはならない。逆にポイ ンタは適切に初期化すれば配列の代用になると言う事も出来る。 int a[10],*p; とした時、sizeof(a)の値は40(10×4バイト)であるが、 sizeof(p)の値はSolarisでは4、Macでは8である。 配列は実際に領域を確保する(正確には 要素数×型の大きさ バイトの 領域)のに対して、ポインタはポインタ変数の1個の大きさ分(Solarisで は4バイト、Macでは8バイト)だけしか確保しない。 配列とポインタは極めて類似していることが分かった。しかし 相違点もある。 Prog-C 2016 Lec05-9 簡単のため、ここでの「配列」とは、1次元配列に限定する a[i], p[i], *(a + i), *(p + i) 配列名が保持する値は実は要素0のアドレスと同じである。 つまり配列名とは配列の要素0を指すポインタのようなものである。 (str &str[0]) 配列aと、配列と同じ型のポインタpがあり p = a; である、つまりpとa の値が同じである時、ポインタpは配列名aの代わりとして使用する 事が出来る。 また、C言語の文法上、配列の添字を示す[i]と、ポインタ演算の+i は同じ意味になるので、以下の書き方は全て同じもの(a[i])を表し ている(次ページのプログラム参照) 文字列の場合、printfなど関数への引数として配列名を渡す。 配列名とはいったい何なのだろうか? 配列名とは Copyright (C) 1999 - 2016 by Programming-C Group 配列 Prog-C 2016 Lec05-12 /home/course/prog1/public_html/2016/lec/source/lec05-5{a,b}.c Copyright (C) 1999 - 2016 by Programming-C Group アドレスが指し示すデータの内容をprintf に渡した後で、アドレスをインクリメント(int なので+4)することを意味する コンパイル結果 : In function `main': : wrong type argument to increment for(i = 0; i < 5; i++) printf("%d\n", *a++); return 0; #include <stdio.h> int main() { int a[5]={1,2,3,4,5}; int i; コンパイル エラー コンパイル、 実行可能! 配列名はアドレス定数であるので、その値(アドレス値)を変更 (代入)出来ない。 #include <stdio.h> int main() { ポインタ int a[5]={1,2,3,4,5}, *p; int i; p = a; for(i = 0; i < 5; i++) printf("%d\n", *p++); return 0; } } ポインタ風 配列風 /home/course/prog1/public_html/2016/lec/source/lec05-4.c 4 ; i++) printf("%d ",*(p + i)); 4 ; i++) printf("%d ",*(a + i)); 4 ; i++) printf("%d ",p[i]); 4 ; i++) printf("%d ",a[i]); 実行結果 std0dc0{s1000001}1: ./a.out 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 std0dc0{s1000001}2: 配列とポインタの相違点 Prog-C 2016 Lec05-10 } for(i = 0 ; i < printf("\n"); for(i = 0 ; i < printf("\n"); for(i = 0 ; i < printf("\n"); for(i = 0 ; i < printf("\n"); return 0; p = a; /* つまりこれは p = &a[0] と同じ事 */ int main() { int i , a[] = {1,2,3,4}; int *p; #include <stdio.h> 配列名とポインタ Copyright (C) 1999 - 2016 by Programming-C Group Prog-C 2016 Lec05-15 文字‘Z’に変更 p printf("%s\n",p); return 0; p[1] = 'Z'; #include <stdio.h> int main() { char *p="ABC"; printf("%s\n",p); 変更可 p="xyz"; return 0; } Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-7c.c 書き込み禁止領域 \0 z y C \0 x B A 実行結果 Segmentation fault (実行結果はマシンの種類によって異なる) 文字列定数はポインタの初期化で宣言する。 文字列定数は一般的にメモリの書き込み禁止領域に領域が取られる このため通常の文字列と異なり、文字列の文字の変更は出来ない(左) ただし、プログラム中で、ポインタの持つアドレスを他の文字列のアドレ スに変更する事は可能である(右) #include <stdio.h> int main() { char *p="ABC"; } 警告 実行すると エラー コンパイル結果 : warning: initialization makes pointer from integer without a cast : warning: excess elements in scalar initializer after `p' コンパイル、 実行可能! /home/course/prog1/public_html/2016/lec/source/lec05-6{a,b}.c 文字列定数の変更 Prog-C 2016 Lec05-13 #include <stdio.h> int main() ポインタ { int i,*p={1,2,3,4,5}; for(i = 0; i < 5; i++) printf("%d\n", p[i]); return 0; } #include <stdio.h> int main() 配列 { int i,a[]={1,2,3,4,5}; for(i = 0; i < 5; i++) printf("%d\n", a[i]); return 0; } 配列は要素の個数分メモリ領域に実際に変数領域が確保される。 一方ポインタを配列の代わりに使用しても、実際には領域の確保 は行われない。従って下例のようにポインタに対して初期化する ことは出来ない。 ポインタに対して定数初期化は出来ない。 配列とポインタの相違点 Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-7{a,b}.c コンパイル、 実行可能! コンパイル、 実行可能! 書き込み禁止領域とは? ユーザがこの場所に配列や変 数を作ることは出来ないが、こ の場合の初期化データはOS (Operating System)を介して書 き込まれる。 p++ p+1 インクリメント 加減算 Prog-C 2016 Lec05-16 p=a 代入 操作 ポインタ 配列とポインタの違いを表にまとめる。 (aを配列名、pをポインタだとする) ○ ○ ○ a+1 a++ a=p ○ × × Copyright (C) 1999 - 2016 by Programming-C Group 配列 配列とポインタの相違点(まとめ) Prog-C 2016 Lec05-14 #include <stdio.h> int main() { char *p="ABC"; printf("%s\n",p); return 0; ポインタ } #include <stdio.h> int main() { char a[]="ABC"; printf("%s\n",a); return 0; 配列 } 文字列ポインタの初期化は、書き込み禁止領域に文字列データが格納され、その アド レスをポインタが指し示すことで行なう。 文字列(文字ポインタ)の場合のみポインタに定数初期化が出来る ポインタ初期化の例外 Prog-C 2016 Lec05-19 ここでint型ポインタpをp = &a[0][1]; とすると、pはa[0][1]を指し、p+5は 配列の行を越え、a[2][0]を指す。 p+5 p : N a g : 高 str[1] str[0] Copyright (C) 1999 - 2016 by Programming-C Group a[2][1] a[2][0] a[1][2] a[1][1] a[1][0] a[0][2] a[0][1] a[0][0] T o k y o \0 アドレス 低 メモリ(変数領域) Copyright (C) 1999 - 2016 by Programming-C Group 前頁では2次元でメモリに格納されるよう書いたが、 実際は右図のように最初の文字列から順に(不定部分も含 め)格納されている。str[0]やstr[1]のように次数を一つ落と した物は各行の先頭の要素を指す。 int型など数字の場合も同じでint a[4][3]; と宣言すると、メ モリ内には右図のように格納される。 メモリ中の2次元配列 Prog-C 2016 Lec05-17 strcpyなどの文字列操作ライブラリ関数も同様に 文字列の開始アドレスを引数として渡す。例えば strcpy(a,&str[1]) は文字列strの2文字目以降 ヌル文字までを文字列aにコピーする。 例えば文字列strの3文字目(str[2])以降(ヌル文字まで) を表示させたければ、printf("%s",&str[2])とすれば良い。 printf("%s",str) のように引数として配列名を書くのは、 「要素0のアドレスを引数として渡す」事を意味する。 (つまりprintf("%s",&str[0])と同じ事である ) 文字列の引数 str[3][1] 文字列と して表示 Prog-C 2016 Lec05-20 文字とし て縦表示 実行結果 std0dc0{s1000001}1: ./a.out Tokyo Nagoya Sapporo A i z u std0dc0{s1000001}2: Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-9.c for(i = 0 ; str[3][i] != '\0' ; i++){ printf("%c\n",str[3][i]); } return 0; for(i = 0 ; i < 3 ; i++){ printf("%s\n",str[i]); } strcpy(str[2],"Sapporo"); /* 文字列の代入可能! */ int main() { char str[4][8] = {"Tokyo","Nagoya","Osaka","Aizu"}; int i; #include <stdio.h> #include <string.h> } Copyright (C) 1999 - 2016 by Programming-C Group の領域は何が入っているか不定 変数領域 \0 文字列配列(2次元文字配列) Prog-C 2016 Lec05-18 A i z u str[3] \0 O s a k a T o k y o \0 N a g o y a \0 str[2] str[1] str[0] char str[4][8] = {"Tokyo","Nagoya","Osaka","Aizu"}; 一つの文字列を表す時には行添え字のみで、str[2]のように表す。(str[2]は3番目 の文字列を示す)また、str[2]の持つアドレスは&str[2][0]と等しい。 文字としてはstr[3][1]のように表す。(4番目の文字列中2番目の文字を示す。) 配列なのでどの場所にも自由に代入を行うことが出来る。(後述のポインタ配列と 比べてみよ) 下記宣言・初期化時の二次元文字配列の状態は以下の通り 文字列配列(2次元文字配列) Copyright (C) 1999 - 2016 by Programming-C Group Prog-C 2016 Lec05-23 str[3] str[2] str[1] ポインタ配列str str[0] str[3][1] \0 \0 Copyright (C) 1999 - 2016 by Programming-C Group 書き込み禁止領域 \0 \0 A i z u O s a k a N a g o y a T o k y o この時のポインタと文字列定数の関係は以下の通り 文字列としてはstr[2]のように表す。(3番目の文字列を示す。) 文字としてはstr[3][1]のように表す。(4番目の文字列中2番目の文字を示す。) 文字列配列とポインタ配列 Prog-C 2016 Lec05-21 同様に2次元文字配列(文字列配列)に対して、文字列定数の配 列はポインタの配列として定義出来る。 文字列配列 char str[2][8] = {"Tokyo","Nagoya"} 文字列定数配列 char *str[2] = {"Tokyo","Nagoya"}; 文字配列(文字列)と文字列定数は以下のように宣言し、書き込 みが出来るかどうかの差異はあるものの、同じに扱う事が出来る。 文字配列 char str[10] = "Aizu" 文字列定数 char *str = "Aizu" 文字列定数配列(ポインタ配列) Copyright (C) 1999 - 2016 by Programming-C Group /home/course/prog1/public_html/2016/lec/source/lec05-8.c 実行結果 std0dc0{s1000001}1: ./a.out Tokyo Nagoya Osaka Aizu std0dc0{s1000001}2: 実行結果 p:8046f4c,&b:8046f4c,b:9999 Prog-0 2016 Lec05-24 Copyright (C) 1999 – 2016 by Programming-0 Group つまりa[4]と言う存在しない配列要素に値を書き込むと、この場合変数bと言うまったく別の変数の 内容が書き換わってしまった事になる。 この例の場合はプログラマーが注意してプログラムを書けば(つまり配列の範囲を超えない)起こり 得ない。しかしながら、正しいプログラムでもユーザーが悪意を持って実行した場合にバッファオー バーフローが起こる例を、Lec15に掲載したバッファオーバーフロー(2)で紹介する。 (続く) int main() { int b = 0, a[4] = {1,2,3,4}, *p; p = &a[4]; *p = 9999; printf("p:%p,&b:%p,b:%d\n",p,&b,b); return 0; } #include <stdio.h> C言語は成り立ちがUnixOSの記述言語であったため、セキュリティに対して割と緩く、例えばポイン タを介して他の変数に書き込む事が可能である。このような動作をバッファオーバーフロー又は、 バッファオーバーランなどと呼ぶ。 例えば左下のプログラムを見てみよう。これをgcc(4.2.1)でコンパイル・実行すると右下のようになる コラム:バッファオーバーフロー(1) Prog-C 2016 Lec05-22 } for(i = 0 ; i < 4 ; i++){ printf("%s\n",str[i]); } return 0; #include <stdio.h> int main() { char *str[] = {"Tokyo","Nagoya","Osaka","Aizu"}; int i; 文字列定数の配列はポインタの配列として定義出来る。 書き込みが出来ない以外は、2次元文字配列と全く同様に扱う事が出来る。 文字列定数配列(ポインタ配列)