Copyright(C) 17Mar2003 coskx
ポインタを使用したプログラムは可読性が低いため,近年ポインタはできるだけ使わないようにする傾向にある。
C言語のポインタは様々な使われ方があるが,ポインタ変数の振る舞いと主な使われ方を以下の順で紹介する。
(1)変数がメモリ上に存在し,コンピュータ内部では変数はアドレスで管理されている。
(2)ポインタ変数にはアドレスを格納できる。
(3)ポインタ変数を使わなければ記述できない配列の動的確保。
(4)関数の型や関数の引数がポインタ変数であることが多いため,これらの関数リファレンスを読んでも困らない程度の知識。
(5)関数で構造体を引数とする場合に用いられる,構造体のアドレス渡し。
この他のトピックとして以下のものがあるが,これは中級向けのものなので省略する。
(1)ポインタ演算
(2)ポインタの配列
(3)関数を指すポインタ
(4)void型のポインタ
(5)2次元配列の動的確保
(6)リストやツリーのデータ構造
9.1 ポインタ変数の導入 |
---|
9.1.1 変数のアドレス |
コンピュータ内部での最小記憶単位はbit(ビット)であり,0または1を記憶することができる。
コンピュータ内部でのデータ操作の単位はbyte(バイト)であり,8bitで構成されている。
byteは8桁の
2進法数値を表すことが出来,00000000から11111111までの値を記憶することができる。10進法で表すと0から255までに対応する。16進法で表すと0x0から0xffまでに対応する。デー
タはbyte単位で扱われ,その大きさの数え方は,1024byteを1kbyte(キロバイト),1024kbyte=1Mbyte(メガバイ
ト),1024Mbyte=1Gbyte(ギガバイト),1024Gbyte=1Tbyte(テラバイト)のようになっている。
コンピュータ内部での変数は,このメモリの一部に保存される。int型変数は連続した4byte,double型変数は連続した8byteが使用されている。
メモリもbyte単位で扱われている。コンピュータのメモリには非常に多くのbyteが並んでいると考えることができる。プログラムからみると,変数はメモリのどこかが使用されているが,どこかを示すためにコンピュータ内部ではアドレスが使われている。
アドレスは0番地から順に1番地,2番地,・・・と言うように数え,各番地には1byteの記憶がなされる。例えば16Gbyteのメモリを載せたコン
ピュータでは,アドレスは0番地から,0x3ffffffff番地まである。(通常アドレスは16進法で表す。)
各番地には16進法で2桁(1byteなので)の値が保存されているので次のようなイメージとなる。
アドレス
保存されているデータ(状況に応じて変わる)
データは8ビットなので16進法で表示すると2桁になる
0x0
0x45
0x1
0xba
0x2
0x5c
0x3
0xf0
:
:
コンピュータが変数の値を操作する時,コンピュータはアドレスを使って変数を特定している。どこのアドレスがその変数に使用されるかはコンパイラシステムやWindowsなどのオペレーティングシステムが決定している。
Cプログラミングではプログラマは変数がどこのアドレスに存在しているのかを考える必要はないが,確認しようと思えば次のように確認することが出来
る。
int myint;
が宣言されている時,
「&myint」の表現は「変数myintが存在しているアドレス」を表す。
(正確には「変数myintが存在している先頭アドレス」というべきである。int型変数は連続4バイトを占めている。)
次の例は「変数myintが存在しているアドレス」を確認するテストプログラムである。
(あくまでも解説用のプログラムであり,実用プログラムでこのようなプログラムを作ることはない。)
List 9.1.1 ある変数がどこのアドレスに存在しているのかを確認するテストプログラム | ||||||||||||
#include <stdio.h> |
||||||||||||
説明
|
重要 変数に&をつけるとその変数のアドレスを得ることができる。 |
9.1.2 アドレスを格納する変数=ポインタ変数 |
アドレスを格納することが出来る変数はポインタ変数と呼ばれる。
ポインタ変数の宣言では,宣言の時に,「*」を変数名の直前につける。またどのような型の変数のアドレスを格納するつもりなのかを明らかにするために,型をつけて宣言する。
次の例は「int型変数のアドレスを格納するポインタ変数 pi
」と「double型変数のアドレスを格納するポインタ変数 pd
」を宣言し,アドレスを格納し,printfでその変数にアドレスが格納されているところを確認するプログラムである。(あくまでも解説用のプログラムであり,実用プログラムでこのようなプログラムを作ることはない。)
List 9.1.2 ポインタ変数 | |||||||||||||||
#include <stdio.h> |
|||||||||||||||
説明
|
「int *pi;」が宣言されるとpiはポインタ変数となり,piにはint型変数のアドレスを格納することができる。
「double
*pd;」が宣言されるとpdはポインタ変数となり,pdにはdouble型変数のアドレスを格納することができる。
重要 ポインタ変数にはアドレスを保存することができる。 |
「int *pi;」が宣言されると「pi」は「int型変数を指すポインタ変数」あるいは単に「intを指すポインタ」と呼ばれる。
「double *pd;」が宣言されると「pd」は「double型変数を指すポインタ変数」あるいは単に「doubleを指すポインタ」と呼ばれる。
注意 「ポインタ」の言葉を「ポインタ変数」の意味に用いる場合と,「指しているアドレス」の意味に用いる場合がある。
本解説では紛らわしい場合には「ポインタ変数」の場合は「ポインタ」とは呼ばないで,必ず「ポインタ変数」と呼ぶこととする。
9.1.3 ポインタ変数のテスト |
プログラムの実行部で,ポインタ変数の直前に「*」をつけると,ポインタ変数が保持しているアドレスにある変数を表す。
次の例は,ポインタ変数の直前に「*」をつけ,ポインタ変数が保持しているアドレスにある変数の値を参照したり,変更するところを確認するプログラムである。(あくまでも解説用のプログラムであり,実用プログラムでこのようなプログラムを作ることはない。)
List 9.1.3 ポインタ変数のテスト | |||||||||||||||
#include <stdio.h> |
|||||||||||||||
説明
|
重要 ポインタ変数に*をつけると,ポインタ変数に保存されているアドレスにある変数を操作できる。 |
まとめ int型変数「myintvar」に値1234が保存されていて,この変数がメモリ内で0x0015ff90に存在すると
変数名 変数のアドレス 変数の値 myintvar 0x0015ff90 1234
&myintvar は 0x0015ff90 であり
int *mypointer;
mypointer=&myintvar;
ならば,*mypointer は1234を表している。
わかりにくいが
int *mypointer=&myintvar;
このように変数宣言時に初期化しても同じ意味になる。
9.1.4 ポインタ変数の利用でよくある間違い |
List 9.1.4 ポインタ変数に意味のあるアドレスを代入せずに,ポインタ変数の指す変数を参照した |
#include <stdio.h> int main() |
コンパイル時に警告が出ました。 xxxx.c(6) : warning C4700: 値が割り当てられていないローカルな変数 'p' に対して参照が行われました。 むりやり,実行したら,実行時エラーで停止しました。 |
List 9.1.5 ポインタ変数に意味のあるアドレスを代入せずに,ポインタ変数の指す変数に値を代入した |
#include <stdio.h> int main() |
コンパイル時に警告が出ました。 xxxx.c(6) : warning C4700: 値が割り当てられていないローカルな変数 'p' に対して参照が行われました。 むりやり,実行したら,実行時エラーで停止しました。 ポインタ変数pにはゴミの値が入っており,その値をアドレスだとして, *p=100; で,そのゴミアドレスに書き込もうとしたので実行時エラーがおこる。 |
List 9.1.6
ポインタ変数にint型変数のアドレスを代入してから,ポインタ変数の指す変数に値を代入した (これは問題ない) |
#include <stdio.h> int main() |
実行結果 *p=100 このプログラムではpにint型変数xのアドレスが入っているため,そのアドレスに値100が書き込まれ, |
コラム scanfの 「&」 |
scanfではキーボードから読み込んだ値を変数に格納してほしい時,変数名に「&」をつけた。 もし,scanfを呼び出すときに「&」を付け忘れたらどうなるだろう。 関数scanfはもらったゴミの値をアドレスとして受取り,キーボード入力された値を,そのアドレスに書き込む。 |
9.2 ポインタ変数とライブラリ関数 |
---|
9.2.1 ポインタ変数と関数puts() |
関数puts()は次のようなプロトタイプ宣言となっている。
int puts( const char *string ) |
stdout ストリーム(ここではコンソール画面)に文字列stringを書き込み,改行します。 返す値:処理が正常に終了した場合負でない値を返します。失敗した場合、puts 関数は EOF を返します。 引数:string 出力すべき文字列の格納場所の先頭アドレスを受け取る |
charを指すポインタ変数が引数となっている。
なお「const」は「この関数内で文字列stringは変更されない」の意味である。
これまで説明してこなかったが,puts()に引数で文字列全体を渡しているのではなく,引数として文字列の先頭要素のアドレス(「文字列の先頭アドレス」と
もいう)を渡していただけである。ただし,文字列の終端には'\0'が入っていることが保障されているため,関数putsは先頭アドレスの提供だけで困ることはない。。
puts(mystring) では,「mystring(例えば "Don't Touch Me."
)という文字列を渡している」と説明してきたが,
実はmystringという配列の先頭アドレスを渡しただけである。
ここでC言語で重要なアドレスに関する文法を紹介する。
char mystring[128]="Don't Touch
Me.";
「配列の名前」は,「配列の先頭要素のアドレス」を意味する。
のもとで,
put(mystring);
というのは,char型の配列mystringの先頭要素のアドレス(「char型の配列mystringの先頭アドレス」ともいう)を渡していることになり,
関数put(const char
*string)に配列mystringの文字列を画面表示してもらうことを意味する。
関数put(const char
*string)から見るとstringに受け取ったアドレスから始まる文字列は'\0'が終端に入っているので,文字列の先頭から'\0'が格納されている直前までを表示するようになっている。
次の例は関数puts()に文字列buff先頭アドレスや途中のアドレスを渡している例である。
「&mystring[0]」は「配列mystringの第0要素
のアドレス」の意味である。
(「mystring」と書いた場合でも,mystringの先頭アドレスを意味している。)
「&mystring[6]」は「配列mystringの第6要素
のアドレス」の意味である。(すなわち文字Tのアドレス)
01234567890123456789
Don't Touch Me.
List 9.2.1 関数puts() |
#include <stdio.h> |
関数putsはC言語の文字列の決まりを使って動作している えらいっ! |
関数putsは文字列の先頭アドレスだけを受け取っている。 先頭アドレスだけ受け取っても,文字列がどこで終わるのかわからなくかったら困ってしまう。 C言語では,文字列の終わりには終端文字\0を置くことが決まりである。 関数putsはこの決まりに従って,先頭文字から終端文字\0の前までを表示している。 |
9.2.2 ポインタ変数と関数gets() |
文字列を操作する関数の説明には,ポインタ変数がよく出てくる。例えば関数gets()は次のようなプロトタイプ宣言となっている。
char *gets( char *buffer ) |
stdin ストリームから 1 行読み込みます。 返す値:成功すると引数の値(読み込み用char配列の先頭番地)を返します。エラーが発生するか、 ファイルの終端を検出すると NULL ポインタを返します。 引数:buffer 入力文字列の格納場所の先頭アドレス |
関数名の直前に「*」が付くと,ポインタ(アドレス)を返す関数(この場合はcharを指すアドレス(ポインタ)を返す関数)
の意味になる。すなわち
char *gets( char *buffer )
は,charを指すポインタ変数が引数となっており,charを指すアドレスを返す関数getsと読む。
ここでC言語で重要なアドレスに関する文法を再度紹介する。
「配列の名前」は,「配列の先頭要素のアドレス」を意味する。 |
char buff[128]; のもとで, gets(buff); というのは,char型の配列buffの先頭要素のアドレス(「char型の配列buffの先頭アドレス」ともいう)を渡していることになり,キーボード入力で得られた文字列をbuffに受け取ることになる。buffの配列サイズは '\0'が入るところも含めて,入力文字数以上でなければならない。 buffに受け取った文字列は'\0'が終端に入っていることが保証されている。 |
なお関数gets()から返された値は普通見ないが,見てもよい。次の例は関数getsの動作を
示す例である。
List 9.2.2 関数gets() |
#include <stdio.h> |
説明 なお「^Z」は WindowsPCの場合であり,UNIXでは「^D」である。 |
9.2.3 ポインタ変数と関数strcpy() |
関数strcpy()は次のようなプロトタイプ宣言となっている。
char *strcpy( char *string1, const char *string2 ) |
文字列をコピーします。 返す値:コピー先の文字列を返します。返すべきエラー値はありません。 引数:string1 コピー先の文字列の先頭アドレス string2 コピー元の文字列の先頭アドレス |
charを指すポインタ変数が引数となっている。コピー元の文字列では文字列の終端にには
'\0'が入っていることを保証しなければならない。コピー先の文字型配列のサイズは'\0'が入るところも含めて,コピー結果が入る大きさでなければな
らない。
次の例は関数strcpyの使用例である。
List 9.2.3 関数strcpy() |
#include <stdio.h> |
コラム scanfと文字列読み込み |
scanfではキーボードから読み込んだ文字列をchar型配列に格納してほしい時,配列名のみ記述した。 |
9.3 ポインタ変数による文字列操作関数 |
---|
9.3.1 ポインタ変数の振る舞いテスト |
文字列は,メモリ上に文字コードが連続して並んでおり,終端は\0になっている。ポインタ変数の振る舞いを見るためのテストプログラムで,確認しよう。
text という表現は,文字列"abc"の先頭アドレス意味している。
実行結果をみると文字列"abc"はアドレス0012FF6Cに存在している。
このことからメモリの状況は次のようになっていると考えられる。
アドレス | 内容 |
0012FF6C |
a |
0012FF6D |
b |
0012FF6E |
c |
0012FF6F |
\0 |
「ptr++」でptrを1増加すると,ptrは0012FF6Cから0012FF6D,0012FF6Eのように増えていく。
「(*ptr)++」だとptrの指しているアドレスにある変数の値が増えてしまうことになるので,
例えばptrが0012FF6Eだったら,このアドレスにある文字コードデータ「c」が「d」になる。
List 9.3.1 charを指すポインタ変数のテスト |
#include "stdio.h" int main() /**************実行結
果********************** |
9.3.2 ポインタ変数を利用した文字列コピー関数 |
文字列操作を行う関数には、ポインタ変数(char
*)がよく使われる。例えば,文字列コピー関数を作ってみると次のようになる。
d++やs++では,これらのポインタ変数に格納されているアドレス値が1つ増加する。
List 9.3.2 文字列コピー関数 |
#include <stdio.h> |
実行結果 |
Hello world Hello world Hello world |
文字列コピー関数に関する補足
ここではポインタ変数に++演算を行なうと,ポインタ変数内のアドレス値は1増加したが,いつもアドレス値が1だけ増えるわけではない。int型を指すポインタ変数ではアドレス値が4ずつ増加する。
ポインタの使いかたは奥が深い。中級になってから学ぶこと。
中級向け補足
9.3.3 ポインタ変数と関数strstr(),strchr() |
関数strstr()は次のようなプロトタイプ宣言となっている。
char *strstr( const char *string1, const char *string2 ) |
第1文字列中に含まれる第2文字列を見つけます。 |
関数strchr()は次のようなプロトタイプ宣言となっている。
char *strchr( const char *string, int c ) |
文字列中に含まれる特定の文字を見つけます。 |
次の例は,文字列「I like travelling and reading.」中に含まれる文字列「ing」の個数を求め,次に文字「a」を「#」に置き換える操作を行なうプログラムである。
List 9.3.7 関数strstr()とstrchr() |
#include <stdio.h> |
NULLに関する補足 (NULLはナルと読む。ヌルは日本語ローマ字読み(ドイツ語読み)である)
NULLはポインタ変数に代入できる値(ポインタ定数)で,どこも指していないことを意味している。
NULLをint型変数やchar型変数に代入するのは意味として誤りである。
(コンパイルエラーにはならないが,警告が表示される。)
NULLは0であると思われているが,これはヘッダーファイル内で定義されているだけで,0である保障はない。
課題 9 その1
(1)文字列の先頭アドレスを受け取り,その文字列の長さを返す関数
int mystrlen(char *str)
を作成し,テスト用mainも作りなさい。 (p09ex01.c)
ただし,関数内でポインタ変数strに関してstr[i]のような文字型配列表現を用いてはならない。
次の検証用mainを使いなさい。
int main()
{
char str1[]="exercises";
char str2[]="pointer programing";
int cnt1,cnt2;
cnt1=mystrlen(str1);
cnt2=mystrlen(str2);
printf("%s %d\n",str1,cnt1);
printf("%s %d\n",str2,cnt2);
return 0;
}
(2)文字列sの左からn文字を文字列dにコピーする関数
void left(char *d,char *s,int n)
を作成しなさい。ただし関数left内でstrで始まるライブラリ関数およびprintfを用いてはならない。
もし文字列sの長さが足りなかったらあるだけコピーするように作りなさい。
(p09ex02.c)
ただし,関数内でポインタ変数d,sに関してd[i],s[i]のような文字型配列表現を用いてはならない。
例えば
char str1[]="exercises";
char str2[32]="abcdefghijklmn"; /*検証のためあらかじめ何らかの文字列を入れておく*/
の時
left(str2,str1,5); → str2には"exerc"がコピーされる。
left(str2,str1,10); → str2には"exercises"コピーされる。
一般的に関数では仕様にない機能はつけてはいけない。
(例えば関数内で画面表示してはならない。デバッグのために関数内で画面表示した場合は,提出時には消しておく。)
次の検証用mainを使いなさい。
int main()
{
char str1[]="exercises";
char str2[32]="abcdefghijklmn";
char str3[32]="abcdefghijklmn";
left(str2,str1,5);
printf("--!%s!--\n",str2); /*--!exerc!--が表示されるはず*/
left(str3,str1,10);
printf("--!%s!--\n",str3); /*--!exercises!--が表示されるはず*/
return 0;
}
(3) 文字列sの右からn文字を文字列dにコピーする関数
void right(char *d,char *s,int n)
を作成しなさい。ただし関数right内でstrで始まるライブラリ関数およびprintfを用いてはならない。
もし文字列sの長さが足りなかったらあるだけコピーするように作る。 (p09ex03.c)
ただし,関数内でポインタ変数d,sに関してd[i],s[i]のような文字型配列表現を用いてはならない。
例えば
char str1[]="exercises";
char str2[32]="abcdefghijklmn"; /*検証のためあらかじめ何らかの文字列を入れておく*/
の時
right(str2,str1,5); → str2には"cises"がコピーされる。
right(str2,str1,10); → str2には"exercises"がコピーされる。
次の検証用mainを使いなさい。
int main()
{
char str1[]="exercises";
char str2[32]="abcdefghijklmn";
char str3[32]="abcdefghijklmn";
char str4[]="Tokyo";
char str5[32]="abcdefghijklmn";
char str6[32]="abcdefghijklmn";
right(str2,str1,5);
printf("--!%s!--\n",str2); /*--!cises!--が表示されるはず*/
right(str3,str1,10);
printf("--!%s!--\n",str3); /*--!exercises!--が表示されるはず*/
right(str5,str4,3);
printf("--!%s!--\n",str5); /*--!kyo!--が表示されるはず*/
right(str6,str4,10);
printf("--!%s!--\n",str6); /*--!Tokyo!--が表示されるはず*/
return 0;
}
(4)文字列sの左からm文字目か
らn文字を文字列dにコピーする関数
void mid(char *d,char *s,int m,int n)
を作成しなさい。
m文字目というのは,1から数えることとする。(例えばabcdefgの4文字目はdである)
ただし関数mid内でstrで始まるライブラリ関数およびprintfを用いてはならない。
もし文字列sの長さが足りなかったらあるだけコピーするように作りなさい。
また,もし文字列sの文字数がm未満だったら,コピーすべき文字列がないので,
dには空の文字列を設置しなさい。(空の文字列=文字列の先頭が'\0')
ただし,関数内でポインタ変数d,sに関してd[i],s[i]のような文字型配列表現を用いてはならない。
(p09ex04.c)
例えば
char str1[]="exercises";
char str2[32]="abcdefghijklmn"; /*検証のためあらかじめ何らかの文字列を入れておく*/
の時
mid(str2,str1,2,5); → str2には"xerci"がコピーされる。
mid(str2,str1,2,10); → str2には"xercises"がコピーされる。
mid(str2,str1,12,3); → str2は空の文字列になる。
次の検証用mainを使いなさい。
int main()
{
char str1[]="exercises";
char str2[32]="abcdefghijklmn";
char str3[32]="abcdefghijklmn";
char str4[32]="abcdefghijklmn";
char str5[]="Tokyo";
char str6[32]="abcdefghijklmn";
char str7[32]="abcdefghijklmn";
puts(str1);
mid(str2,str1,2,5);
printf("--!%s!--\n",str2); /*--!xerci!--が表示されるはず*/
mid(str3,str1,2,10);
printf("--!%s!--\n",str3); /*--!xercises!--が表示されるはず*/
mid(str4,str1,12,3);
printf("--!%s!--\n",str4); /*--!!--が表示されるはず*/
mid(str6,str5,2,1);
printf("--!%s!--\n",str6); /*--!o!--が表示されるはず*/
mid(str7,str5,2,10);
printf("--!%s!--\n",str7); /*--!okyo!--が表示されるはず*/
return 0;
}
(5)文字列sを逆順に並べ替える関数
void reverseString(char *s)
を作成しなさい。ただし関数reverseString内でstrで始まるライブラリ関数およびprintfを用いてはならない。
(p09ex05.c)
ただし,関数内でポインタ変数sに関してs[i]のような文字型配列表現を用いてはならない。
例えば
char str[]="exercises";
の時
reverseString(str); → strは"sesicrexe"に変更される。
次の検証用mainを使いなさい。
int main()
{
char str[30]="exercises";
char str2[30]="java";
printf("--!%s!--\n",str);
reverseString(str);
printf("--!%s!--\n",str);
printf("--!%s!--\n",str2);
reverseString(str2);
printf("--!%s!--\n",str2);
return 0;
}
9.4 ポインタ変数による配列の動的確保 |
---|
これまで,配列は次のように宣言され,変数宣言の時点でその大きさが決まっていた。
int garray[1000];
int main()
{
int array[100];
:
このような配列は静的配列と呼ばれる。
ところで,プログラムが起動後に配列のサイズがを決めたいがある。例えば,
int main()
{
int size;
:
scanf("%d",&size);
int array[size];
:
のようにしたいことがある。このように,プログラムが起動してから配列の大きさを決めることができれば,
その配列は動的配列となる。
しかし,変数は実行文より前に宣言されなかればならないため,このようなプログラムはコンパイルエラーになってしまう。
それでは,ユーザ関数内での配列を作ったらどうだろう。
int myfunc(int size)
{
int array[size];
:
このようなプログラムなら,実行文より前に配列変数の宣言ができる。
しかし残念ながら,これもコンパイルエラーになってしまう。
配列サイズを書くときは定数である必要があり,変数(上記例ではsize)を用いることは出来ない。
そこで,配列の大きさをプログラム起動後に決める方法は,配列の動的確保と呼ばれる方法である。
この方法ではポインタを使う必要がある。
次のような配列はポインタによる動的確保をするのが好ましい。
(1)1Mバイトを超えるような大きな配列
(2)プログラミングの時点で大きさが確定せず,プログラムが起動してから大きさが確定する配列
配列の動的管理は次の手順による。
1 | 配列を作る予定の型のポインタ変数を用意する |
2 | 関数malloc(),あるいは関数calloc()により必要な配列のためのメモリを確保し,返された値をポインタ変数に格納する |
3 | ポインタ変数を配列と同じように使用する |
4 | 作業が終わったら関数free()でメモリを解放する |
List 9.4.1 大きな配列の動的確保 | ||
#include <stdio.h> |
||
追加説明 | ||
(1)関数malloc
(2)sizeof演算子
|
List 9.4.2 プログラム起動後に大きさの決まる配列の動的確保 |
#include <stdio.h> |
配列確保と配列開放を関数化すると次のようになる。関数を再利用することにすると,呼び出し側 を作る時に楽が出来る。
List 9.4.3 int型配列動的確保の関数化 |
#include <stdio.h> |
前の例はint型の配列の確保の関数だが,拡張して,どのような型の配列にでも対応できるようにする。
List 9.4.4 配列動的確保の関数化 |
#include <stdio.h> |
どうしてポインタ変数が配列変数に変わってしまうかというと,次の理由による。
要点 逆に int array[10] |
補足 mallocで確保できる大きさ
mallocではどのくらい大きな領域(バイト数で数える)を取ることができるのかというと,それは使っているシステムに依存する。
現在使っているコンパイラでは,整数を表わすのは32bitであり,その最大値429496729である。
malloc(size*sizeofvariable);このように使うときには,size*sizeofvariableの値が整数で表わせる値を超えないようにする必要がある。
実は,mallocの引数はsize_t型となっていて,size_t型のビット数はコンパイラに依存している。
(現在使用しているコンパイラではsize_tは32bit)
mallocが返すポインタ変数にしても,現在使っているコンパイラでは,32ビットであるが,64ビット,128ビットを使うシステムもある。
また実際,実装されているメモリの大きさに依存することも考えられるが,不足分は外部記憶(ハードディスクなど)上の仮想メモ
リを使うシステムもあり,使えるメモリの大きさはOSに依存する。
小さなマイコンでは,実装されているメモリの大きさに依存するため,注意が必要である。
補足 realloc
配列を動的に確保したが,作業中に足りなくなって,もっと大きな配列に変更したくなることがある。
(多くの場合,最初の見通しが甘かったことになるが)
そのような場合はrealloc関数を用いると,配列の取り直しと,中身のコピーが行われ,最初から大きな
配列が確保されているように使える。
reallocの引数は2つあり,第1引数は古い領域の先頭アドレス,第2引数は新たに確保要求するバイト数である。
関数が正常に動作した場合,使い終えた領域(第1引数)はreallocが開放しているため,
realloc関数終了後に開放する必要はない。
realloc終了後にfree操作をすると二重解放となり,実行時に問題を起こす。
List 9.4.5 配列の動的再確保 #include <stdio.h>
#include <stdlib.h>
int main()
{
int *ary,*tmp;
int i;
ary=(int *)malloc( 100 *sizeof(int));
if (ary==NULL) {
printf("ary[100]の確保に失敗しました\n");
return (1);
}
for (i=0; i<100; i++) ary[i]=i;
tmp=ary;
ary=(int *)realloc(tmp, 200 *sizeof(int));
printf("A ary=%p\n",ary);
printf("A tmp=%p\n",tmp);
if (ary==NULL) {
printf("ary[200]の確保に失敗しました\n");
free(tmp);
return (1);
}
for (i=100; i<200; i++) ary[i]=i;
tmp=ary;
ary=(int *)realloc(tmp, 300 *sizeof(int));
printf("B ary=%p\n",ary);
printf("B tmp=%p\n",tmp);
if (ary==NULL) {
printf("ary[300]の確保に失敗しました\n");
free(tmp);
return (1);
}
for (i=200; i<300; i++) ary[i]=i;
tmp=ary;
ary=(int *)realloc(tmp, 400 *sizeof(int));
printf("C ary=%p\n",ary);
printf("C tmp=%p\n",tmp);
if (ary==NULL) {
printf("ary[400]の確保に失敗しました\n");
free(tmp);
return (1);
}
for (i=300; i<400; i++) ary[i]=i;
tmp=ary;
ary=(int *)realloc(tmp, 500 *sizeof(int));
printf("D ary=%p\n",ary);
printf("D tmp=%p\n",tmp);
if (ary==NULL) {
printf("ary[500]の確保に失敗しました\n");
free(tmp);
return (1);
}
for (i=400; i<500; i++) ary[i]=i;
for (i=0; i<500; i++) printf("%4d",ary[i]);
free(ary);
return 0;
}
/* 実行結果
>realloctest.exe
A ary=003D96F0
A tmp=003D96F0
B ary=003DAA20
B tmp=003D96F0
C ary=003DAA20
C tmp=003DAA20
D ary=003DAA20
D tmp=003DAA20
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
*/
課題 9 その2
(6)配列の動的確保の手法と「エラトステネスのふるい」を用いてnまでの素数を求めるプログラムを作成しなさい。 (p09ex06.c)
ただし,nはプログラムが開始してから,キーボードより与えるものとし,nを受け取ってからふさわしい大きさの配列を動的に確保するものとします。(nは配列の大きさと一致するとは限らない。)
見つかった素数は,書式 "%7d" で表示し,10個ごとに改行しなさい。
またすべての素数を表示した後,改行して,見つかった素数の個数を次の書式で表示しなさい。
"%d prime numbers are found.\n"
実行時にエラーが出る場合は,配列のサイズ以上の要素をアクセスしていないか確かめる必要がある。
提出ファイルではn=100,n=101,n=10000のときについて実行結果をすべて貼り付けなさい。
この3つの実行時には,実行時エラーが出ていないことを必ず確かめなさい。
ヒント 「エラトステネスのふるい」は「4.配列と文字・文字列」の課題(18)で作成しています。
9.5 関数への構造体のアドレス渡し |
---|
9.5.1 構造体をコピーして渡す引数と,アドレスのみ渡す引数
例えば
typedef struct { int x; /*x座標*/ int y; /*y座標*/ } point_t; |
List 9.5.1 構造体を引数に受け取る関数printPoint() |
#include <stdio.h> |
List 9.5.2 構造体のアドレスを引数に受け取る関数printPoint() |
#include <stdio.h> |
9.5.2 演算子「->」の導入
List 9.5.2の関数printPointとまったく同じ機能でC言語特有な表現をすると次のようになる。
表現が異なる(記述が簡単になる??)だけで,動作は全くおなじになる。
List 9.5.3 構造体のアドレスを引数に受け取る関数printPoint() |
#include <stdio.h> |
List 9.5.4 構造体のアドレスを引数に受け取る関数printPoint() |
#include <stdio.h> |
説明 |
9.5.4 アドレス渡しされた構造体の要素を変更する関数
List 9.5.5の関数
scalePoint(point_t *point, int
scale)は,構造体のアドレスを渡されている。そのため,作業している構造体は呼び出した側が確保している構造体である。見かけ上,関数内で構造体要
素を変更して呼び出し側に返すことができているように見える。
これに対して,scalePointNG(point_t
point, int
scale)は,呼び出し側の構造体のコピーが関数に渡されている。そのため,いくら構造体の値を変化させても,関数内の構造体の値が変化しているだけ
で,呼び出し側の構造体に影響を与えることはできない。
List 9.5.5 構造体のアドレスを引数に受け取る関数scalePoint()は,構造体の中身を変更して返すことが出来る |
#include <stdio.h> |
課題 9 その3 |
---|
時間を表す構造体は次のようになる。
typedef struct {
int hour; /*時間を表す*/
int min; /*分を表す*/
int sec; /*秒を表す*/
} hmstime_t;
時間の和を求める関数
hmstime_t addTime(hmstime_t *t1, hmstime_t *t2)
を作りなさい。
2時間46分27秒+1時間34分34秒の答えを求める時は
hmstime_t t1={2,46,27};
hmstime_t t2={1,34,34};
hmstime_t t3;
のもとで
t3=addTime(&t1,&t2);
と書けるようにしなさい。
次の検証用mainを使いなさい。
int main()
{
hmstime_t t1={2,46,27};
hmstime_t t2={1,34,34};
hmstime_t t3;
hmstime_t t4={2,46,27};
hmstime_t t5={1,13,34};
hmstime_t t6;
t3=addTime(&t1,&t2);
t6=addTime(&t4,&t5);
printf("%2d:%2d:%2d\n",t3.hour,t3.min,t3.sec);
printf("%2d:%2d:%2d\n",t6.hour,t6.min,t6.sec);
return 0;
}
9.6 関数のラップ |
---|
9.6.1 ライブラリ関数fgetc()とポインタFILE *fp
最初に復習。関数fgetc()を使って,ファイル内容をそのまま画面に出力してみよう。
list 9.6.1 ファイル読み込みと表示 |
#include <stdio.h> #include <stdlib.h> int main() { FILE *fp; int ch; fp=fopen("test1.txt","r"); if (fp==NULL) exit(1); ch=fgetc(fp); while (ch!=EOF) { putchar(ch); ch=fgetc(fp); } fclose(fp); return 0; } |
ファイルtest1.txtの内容 |
Hello How are you. @Good day! I have a @bad!news. |
処理結果の画面表示 |
Hello How are you. @Good day! I have a @bad!news. |
int fgetc( FILE *fp ) |
ファイルストリームfpから1文字読み込みます。 返す値:処理が正常に終了した場合読み込んだ文字の文字コードをを返します。失敗した場合は EOF を返します。 引数:fp 読み込むファイルストリーム FILE構造体のアドレスが入っているポインタ変数 FILE *fp; fp=fopen("filetoread.txt","r"); : ch=fgetc(fp); のように使われる。 |
コラム 「 FILE *fp 」とは何だったのか
FILE型とは,ファイル読み書きのときに使われる構造体であ り,読み書きの対象を具体的なファイルの実態と結び付け,読み込みバッファや書き出しバッファのどこを作業しているのかのポインタなども内部に持ってい る。その内部構成はヘッダーファイルで定義されており,コンパイラ付属のライブラリに依存している。
fpはその構造体の先頭アドレスを保持しているポインタ変数である。
fp=fopen("xxxxx.txt","r");
を行うと,関数fopen()はファイルxxxxx.txtを対象に読み込む作業をするための構造体を作り,そのアドレスを返してくる。
(構造体が作られるときには関数mallocなどが使われてメモリ確保が行われていることが推測される。)
もし,何らかの原因で,構造体が作れない場合はNULLポインタを返してくる。だからfp==NULLの場合には作業を打ち切るようにプログラムを書い た。そして,「fscanf(fp,"%d",&x)」のようにここでもまたポインタ変数fpを与えて読み込みを行った。
また,ファイルに対する作業が終わったときには「fclose(fp)」のようにポインタfpを渡して構造体を破棄していた。
(構造体が破棄されるときには関数freeが使われていることが推測される。)
9.6.2 ユーザ関数myfgetc()をつくる
ここで次のような課題を考えよう。
ファイルtest1.txtの内容 |
Hello How are you. @Good day! I have a @bad!news. |
処理結果の画面表示 |
Hello How are you. I have a news. |
list 9.6.2 ファイル読み込みと表示制御 その1 |
#include <stdio.h> #include <stdlib.h> int main() { FILE *fp; int ch; int nowork=0; fp=fopen("test1.txt","r"); if (fp==NULL) exit(1); ch=fgetc(fp); while (ch!=EOF) { if (nowork==0) { if (ch=='@') { nowork=1; } else { putchar(ch); } } else { if (ch=='!') { nowork=0; } } ch=fgetc(fp); } fclose(fp); return 0; } |
list 9.6.3 ファイル読み込みと表示制御 その2 |
#include <stdio.h> #include <stdlib.h> int myfgetc(FILE *fp) { int ch; ch=fgetc(fp); while(ch!=EOF && ch=='@') { //ここがifではなくwhileになっていることに注意*1 ch=fgetc(fp); while(ch!=EOF && ch!='!') { ch=fgetc(fp); } ch=fgetc(fp); //*2 } return ch; } int main() { FILE *fp; int ch; fp=fopen("test1.txt","r"); if (fp==NULL) exit(1); ch=myfgetc(fp); while (ch!=EOF) { putchar(ch); ch=myfgetc(fp); } fclose(fp); return 0; } |
int myfgetc( FILE *fp ) |
ファイルストリームfpから1文字読み込みます。 返す値:処理が正常に終了した場合読み込んだ文字の文字コードを返します。失敗した場合は EOF を返します。 ただし,読み込み文字列中に「@」が見つかった場合,「!」が見つかるところまでを削除します。 引数:fp 読み込むファイルストリーム FILE構造体のアドレスが入っているポインタ変数 FILE *fp; fp=fopen("filetoread.txt","r"); : ch=myfgetc(fp); のように使われる。 |
9.6.3 ユーザ関数fgettoken()を作る
ファイルから,文字列を文字列毎に取り出して画面表示したい場合がある。文字列の区切りは,スペース,改行,タブ(これらは複数個連続していても良い)とする。
例えば先のtest1.txtの処理は次のようにしたい。
ファイルtest1.txtの内容 |
Hello How are you. @Good day! I have a @bad!news. |
処理結果の画面表示 |
Hello How are you. @Good day! I have a @bad!news. |
list 9.6.4 ファイルからのトークンの読み込み |
#include <stdio.h> #include <stdlib.h> #include <ctype.h> // isspace()のため char *fgettoken(char *buff, int size, FILE *fp) { int ch; int crr=0; ch=fgetc(fp); while (ch!=EOF && isspace(ch)) { //isspace()はchが区切り文字かどうかをチェックする関数 ch=fgetc(fp); } while (ch!=EOF && !isspace(ch)) { if (crr<size-2) { buff[crr++]=ch; } else { break; } ch=fgetc(fp); } if (ch==EOF && crr==0) { //ファイル終端に達した場合への対処 return NULL; } else { buff[crr]='\0'; return buff; } } #define SIZE 256 int main() { FILE *fp; char buff[SIZE]; char *ptr; fp=fopen("test1.txt","r"); if (fp==NULL) exit(1); ptr=fgettoken(buff,SIZE,fp); while (ptr!=NULL) { puts(buff); ptr=fgettoken(buff,SIZE,fp); } fclose(fp); return 0; } |
char *fgets( char *buff, int size, FILE *fp ) char *fgets( char buff[], int size, FILE *fp ) と同じ |
ファイルストリームfpから1行読み込み,保存領域buffに保存します。ただし保存領域の大きさはsize byteです。 返す値:処理が正常に終了した場合,保存領域の先頭アドレスを返します。失敗した場合は ポインタ定数NULL を返します。 |
char *fgettoken(char *buff, int size, FILE *fp) |
ファイルストリームfpから1トークン読み込み,保存領域buffに保存します。ただし保存領域の大きさはsize byteです。 返す値:処理が正常に終了した場合,保存領域の先頭アドレスを返します。失敗した場合は ポインタ定数NULL を返します。 |
list 9.6.5 ファイルからのトークンの読み込み(@!間を無視するルール追加) |
#include <stdio.h> #include <stdlib.h> #include <ctype.h> // isspace()のため int myfgetc(FILE *fp) { int ch; ch=fgetc(fp); while(ch!=EOF && ch=='@') { ch=fgetc(fp); while(ch!=EOF && ch!='!') { ch=fgetc(fp); } ch=fgetc(fp); } return ch; } char *fgettoken(char *buff, int size, FILE *fp) { int ch; int crr=0; ch=myfgetc(fp); while (ch!=EOF && isspace(ch)) { ch=myfgetc(fp); } while (ch!=EOF && !isspace(ch)) { if (crr<size-2) { buff[crr++]=ch; } else { break; } ch=myfgetc(fp); } if (ch==EOF && crr==0) { //ファイル終端に達した場合への対処 return NULL; } else { buff[crr]='\0'; return buff; } } #define SIZE 256 int main() { FILE *fp; char buff[SIZE]; char *ptr; fp=fopen("test1.txt","r"); if (fp==NULL) exit(1); ptr=fgettoken(buff,SIZE,fp); while (ptr!=NULL) { puts(buff); ptr=fgettoken(buff,SIZE,fp); } fclose(fp); return 0; } |
実行結果 |
Hello How are you. I have a news. |
おまけ
もし,取り出したトークン(文字列buff)が数字だけでできていて,これをint型の変数varに取り込みたかったら,
sscanf(buff,"%d",&var);
のようにするとよい。